Add agent.py

This commit is contained in:
2026-06-02 14:52:10 +00:00
parent 168e892a6a
commit 9c191680c3
+166
View File
@@ -0,0 +1,166 @@
"""
LangGraph Agent that plans and executes a task step by step.
Usage:
python agent.py "Compare Python and JavaScript"
Dependencies:
- langgraph
- langchain-openai
- langchain-ollama (optional)
"""
from __future__ import annotations
import json
import os
import re
import sys
import traceback
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
# ----- State definition (from snippet) -----
class PlanningState(TypedDict):
task: str
plan: list[str] | None
current_step: int
results: list[str]
# ----- LLM configuration -----
# Prefer OpenAI if API key is set; otherwise fallback to Ollama
if os.getenv("OPENAI_API_KEY"):
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
)
else:
# Fall back to local Ollama model if available
llm = ChatOpenAI(
model=os.getenv("CHAT_MODEL", "llama3"),
base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1"),
api_key="ollama",
temperature=0,
)
# ----- Planning node -----
def planning(state: PlanningState) -> PlanningState:
"""Prompt LLM to break the task into 36 numbered steps."""
try:
prompt = (
f"Given the task '{state['task']}', break it into 3-6 numbered steps. "
"Return only the numbered list or a JSON array of steps."
)
raw = llm.invoke(prompt)
text = raw if isinstance(raw, str) else raw.content
except Exception as e:
raise RuntimeError(f"LLM failed in planning node: {e}")
# Parse plan first try JSON, then regex
plan: list[str] | None = None
try:
plan = json.loads(text)
if not isinstance(plan, list):
plan = None
except Exception:
plan = None
if plan is None:
pattern = r"^\s*\d+\.\s+(.*)$"
plan = [m.group(1).strip() for m in re.finditer(pattern, text, re.MULTILINE)]
if not plan or not (3 <= len(plan) <= 6):
raise ValueError(
f"Planning node returned invalid plan: {plan}. Expected 3-6 steps."
)
state["plan"] = plan
state["current_step"] = 0
state["results"] = []
return state
# ----- Execution node -----
def execution(state: PlanningState) -> PlanningState:
"""Execute a single step from the plan and record the result."""
step_idx = state["current_step"]
if state["plan"] is None or step_idx >= len(state["plan"]):
return state
step = state["plan"][step_idx]
try:
prompt = f"Execute step: {step}\nProvide a concise result."
raw = llm.invoke(prompt)
result = raw if isinstance(raw, str) else raw.content
except Exception as e:
result = f"Error executing step: {e}"
state["results"].append(result.strip())
state["current_step"] = step_idx + 1
return state
# ----- Graph construction -----
# Graph diagram (from snippet):
# START → planning → execution → should_continue
# ↑____________| (execute)
# finish → END
builder = StateGraph(PlanningState)
builder.add_node("planning", planning)
builder.add_node("execution", execution)
# Conditional edges to loop execution until all steps processed
builder.add_conditional_edges(
"planning",
lambda state: "execution" if state.get("plan") else "END"
)
builder.add_conditional_edges(
"execution",
lambda state: "execution"
if state.get("current_step", 0) < len(state.get("plan", []))
else "END"
)
builder.set_entry_point("planning")
# Compile graph with in-memory checkpointing
graph = builder.compile(checkpointer=InMemorySaver())
# ----- Demo -----
def run_demo(task: str) -> None:
initial_state: PlanningState = {
"task": task,
"plan": None,
"current_step": 0,
"results": [],
}
try:
result = graph.invoke(initial_state)
except Exception:
traceback.print_exc()
sys.exit(1)
plan = result.get("plan", [])
print("\n===== PLAN =====")
for idx, step in enumerate(plan, 1):
print(f"{idx}. {step}")
print("\n===== RESULTS =====")
for idx, res in enumerate(result.get("results", []), 1):
print(f"[Step {idx}] {res}\n")
print("===== SUMMARY =====")
try:
summary_prompt = f"Given the collected results: {result.get('results', [])}, produce a concise summary of the task outcome."
summary = llm.invoke(summary_prompt)
print(summary if isinstance(summary, str) else summary.content)
except Exception as e:
print(f"Error generating summary: {e}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python agent.py '<task>'")
sys.exit(1)
task = " ".join(sys.argv[1:])
run_demo(task)