174 lines
6.1 KiB
Python
174 lines
6.1 KiB
Python
"""
|
||
Main entry point for the "Planning Agent" assignment.
|
||
|
||
The project demonstrates a LangGraph agent that first plans a task into discrete steps and then executes those steps one by one.
|
||
|
||
Examples are provided in the ``__main__`` section – run the script with different tasks to see how the planner works.
|
||
|
||
Requirements:
|
||
- langgraph>=0.2.0
|
||
- langchain-openai>=0.3.0
|
||
- python-dotenv>=1.0.0
|
||
- rich>=13.0.0
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import TypedDict, List, Optional
|
||
|
||
from langgraph.graph import StateGraph, START, END
|
||
from langchain_core.messages import HumanMessage, SystemMessage
|
||
from langchain_openai import ChatOpenAI
|
||
from rich.console import Console
|
||
from rich.table import Table
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. State definition
|
||
# ---------------------------------------------------------------------------
|
||
class PlanningState(TypedDict):
|
||
task: str
|
||
plan: List[str] | None
|
||
current_step: int
|
||
results: List[str]
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. LLM configuration – BroJS endpoint
|
||
# ---------------------------------------------------------------------------
|
||
llm = ChatOpenAI(
|
||
model="openai/gpt-oss-20b:free",
|
||
base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1",
|
||
api_key=os.getenv("JOURNAL_MCP_PAT"),
|
||
temperature=0.2,
|
||
)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. Planning node – split task into steps
|
||
# ---------------------------------------------------------------------------
|
||
def planning(state: PlanningState) -> PlanningState:
|
||
"""Ask the LLM to produce a numbered list of steps.
|
||
|
||
The prompt forces JSON output for reliable parsing.
|
||
"""
|
||
system = SystemMessage(
|
||
content="You are a helpful assistant that splits a task into clear, actionable steps. Return a JSON array of strings under the key `plan`."
|
||
)
|
||
user = HumanMessage(content=f"Plan the following task: {state['task']}")
|
||
|
||
response = llm.invoke([system, user])
|
||
text = response.content.strip()
|
||
|
||
# Try to parse JSON; if fails, fall back to simple split by lines.
|
||
try:
|
||
import json
|
||
data = json.loads(text)
|
||
plan: List[str] = data.get("plan", [])
|
||
except Exception:
|
||
# Fallback – split on newlines and strip numbering
|
||
plan = []
|
||
for line in text.splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
# Remove leading numbers like "1. " or "- "
|
||
if line[0].isdigit() and (len(line) > 2 and line[1] in ".-"):
|
||
line = line.split("", 1)[1]
|
||
plan.append(line)
|
||
return {
|
||
**state,
|
||
"plan": plan,
|
||
"current_step": 0,
|
||
"results": [],
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. Execution node – perform one step (here we just echo the step)
|
||
# ---------------------------------------------------------------------------
|
||
def execution(state: PlanningState) -> PlanningState:
|
||
"""Execute a single step.
|
||
|
||
In a real assignment you would replace this with calls to tools or other logic.
|
||
For demonstration, we simply record the step text as the result.
|
||
"""
|
||
idx = state["current_step"]
|
||
if idx >= len(state["plan"]):
|
||
return state
|
||
step_text = state["plan"][idx]
|
||
# Simulate execution – in practice you might call a tool here.
|
||
result = f"Executed: {step_text}"
|
||
new_results = state["results"].copy()
|
||
new_results.append(result)
|
||
return {
|
||
**state,
|
||
"current_step": idx + 1,
|
||
"results": new_results,
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. Decision node – should we continue?
|
||
# ---------------------------------------------------------------------------
|
||
def should_continue(state: PlanningState) -> str:
|
||
if state["current_step"] >= len(state.get("plan", [])):
|
||
return "finish"
|
||
return "execute"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6. Build the graph
|
||
# ---------------------------------------------------------------------------
|
||
graph = StateGraph(PlanningState)
|
||
graph.add_node("planning", planning)
|
||
graph.add_node("execution", execution)
|
||
graph.add_conditional_edges(
|
||
"planning",
|
||
lambda _: "execute" if _.get("plan") else "finish",
|
||
)
|
||
graph.add_edge("execution", "should_continue")
|
||
graph.add_conditional_edges(
|
||
"should_continue",
|
||
should_continue,
|
||
{"execute": "execution", "finish": END},
|
||
)
|
||
agent = graph.compile()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 7. Helper to run the agent and pretty‑print results
|
||
# ---------------------------------------------------------------------------
|
||
def run_agent(task: str) -> None:
|
||
console = Console()
|
||
state: PlanningState = {"task": task, "plan": None, "current_step": 0, "results": []}
|
||
config = {"configurable": {"thread_id": f"{task[:8]}"}}
|
||
|
||
# Run the agent – we capture intermediate states via a callback.
|
||
results: List[PlanningState] = []
|
||
for event in agent.stream(state, config):
|
||
if isinstance(event, dict) and "messages" in event:
|
||
continue # ignore final message
|
||
results.append(event)
|
||
|
||
# Print plan
|
||
console.print("\n[bold underline]Task:[/]", task)
|
||
if state.get("plan"):
|
||
table = Table(title="Plan", show_header=False, box=None)
|
||
for i, step in enumerate(state["plan"], 1):
|
||
table.add_row(f"{i}. {step}")
|
||
console.print(table)
|
||
|
||
# Print execution results
|
||
console.print("\n[bold underline]Execution Results:[/]")
|
||
for r in state.get("results", []):
|
||
console.print(r)
|
||
|
||
console.print("\n[green]Finished.[/]\n")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 8. Demo examples
|
||
# ---------------------------------------------------------------------------
|
||
if __name__ == "__main__":
|
||
examples = [
|
||
"Compare Python and JavaScript in web development.",
|
||
"Plan a weekend trip to the mountains.",
|
||
"Explain how a neural network learns.",
|
||
]
|
||
for ex in examples:
|
||
run_agent(ex)
|