"""Планирующий LangGraph-агент: planning → execution (цикл) → итог.""" from __future__ import annotations import json import os import re import sys from typing import Literal, TypedDict from dotenv import load_dotenv from langchain_core.messages import HumanMessage, SystemMessage from langchain_openai import ChatOpenAI from langgraph.graph import END, START, StateGraph load_dotenv() BROJS_INFERENCE_URL = "https://platform.brojs.ru/jrnl-bh/api/inference/v1" DEFAULT_MODEL = "openai/gpt-oss-20b:free" DEFAULT_TASK = "Сравни Python и JavaScript" class PlanningState(TypedDict, total=False): task: str plan: list[str] | None current_step: int results: list[str] summary: str def _api_key() -> str: return ( os.getenv("JOURNAL_MCP_PAT") or os.getenv("JOURNAL_TOKEN") or os.getenv("OPENAI_API_KEY") or "" ) def _base_url() -> str: if os.getenv("OPENAI_BASE_URL"): return os.environ["OPENAI_BASE_URL"] if os.getenv("OPENAI_API_KEY") and not os.getenv("JOURNAL_MCP_PAT"): return os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1") return BROJS_INFERENCE_URL def build_llm() -> ChatOpenAI: return ChatOpenAI( model=os.getenv("OPENAI_MODEL", DEFAULT_MODEL), base_url=_base_url(), api_key=_api_key(), temperature=0.2, ) def _parse_plan(text: str) -> list[str]: text = text.strip() if not text: return [] try: data = json.loads(text) if isinstance(data, dict) and isinstance(data.get("steps"), list): return [str(s).strip() for s in data["steps"] if str(s).strip()] if isinstance(data, list): return [str(s).strip() for s in data if str(s).strip()] except json.JSONDecodeError: pass steps: list[str] = [] for line in text.splitlines(): line = line.strip() if not line: continue m = re.match(r"^[\d\-\*\.)]+\s*(.+)$", line) steps.append((m.group(1) if m else line).strip()) return [s for s in steps if s][:6] def planning(state: PlanningState, llm: ChatOpenAI) -> dict: task = state.get("task", DEFAULT_TASK) prompt = ( "Разбей задачу на 3–6 конкретных шагов.\n" "Верни ТОЛЬКО JSON: {\"steps\": [\"шаг 1\", \"шаг 2\", ...]}\n\n" f"Задача: {task}" ) raw = llm.invoke( [ SystemMessage(content="Ты планировщик. Отвечай только валидным JSON."), HumanMessage(content=prompt), ] ).content plan = _parse_plan(str(raw)) if len(plan) < 3: plan = [ "Собрать ключевые характеристики первого объекта", "Собрать ключевые характеристики второго объекта", "Сравнить сходства и различия", "Сформулировать вывод", ] return {"plan": plan, "current_step": 0, "results": []} def execution(state: PlanningState, llm: ChatOpenAI) -> dict: plan = state.get("plan") or [] idx = int(state.get("current_step", 0)) results = list(state.get("results") or []) task = state.get("task", DEFAULT_TASK) step_text = plan[idx] prompt = ( f"Общая задача: {task}\n" f"Текущий шаг ({idx + 1}/{len(plan)}): {step_text}\n\n" "Выполни только этот шаг. Ответ — 2–5 предложений." ) raw = llm.invoke([HumanMessage(content=prompt)]).content step_result = str(raw).strip() results.append(step_result) return {"results": results, "current_step": idx + 1} def finish(state: PlanningState, llm: ChatOpenAI) -> dict: task = state.get("task", DEFAULT_TASK) plan = state.get("plan") or [] results = state.get("results") or [] parts = [f"Шаг {i + 1}: {plan[i]}\n{r}" for i, r in enumerate(results)] body = "\n\n".join(parts) prompt = ( f"Задача: {task}\n\nРезультаты по шагам:\n{body}\n\n" "Сформируй краткую итоговую сводку (4–6 предложений)." ) raw = llm.invoke([HumanMessage(content=prompt)]).content return {"summary": str(raw).strip()} def should_continue(state: PlanningState) -> Literal["execute", "finish"]: plan = state.get("plan") or [] if int(state.get("current_step", 0)) >= len(plan): return "finish" return "execute" def build_graph(llm: ChatOpenAI | None = None): llm = llm or build_llm() def _planning(state: PlanningState) -> dict: return planning(state, llm) def _execution(state: PlanningState) -> dict: return execution(state, llm) def _finish(state: PlanningState) -> dict: return finish(state, llm) graph = StateGraph(PlanningState) graph.add_node("planning", _planning) graph.add_node("execution", _execution) graph.add_node("finish", _finish) graph.add_edge(START, "planning") graph.add_edge("planning", "execution") graph.add_conditional_edges( "execution", should_continue, {"execute": "execution", "finish": "finish"}, ) graph.add_edge("finish", END) return graph.compile() def run_demo(task: str = DEFAULT_TASK) -> PlanningState: app = build_graph() initial: PlanningState = { "task": task, "plan": None, "current_step": 0, "results": [], "summary": "", } print(f"Задача: {task}\n") final: PlanningState = dict(initial) for event in app.stream(initial, stream_mode="updates"): for node, update in event.items(): if not isinstance(update, dict): continue final = {**final, **update} if node == "planning" and final.get("plan"): print("План:") for i, step in enumerate(final["plan"], 1): print(f"{i}. {step}") print() elif node == "execution" and final.get("results"): n = len(final["results"]) print(f"[Шаг {n}] {final['results'][-1]}\n") elif node == "finish": print(f"Итог: {final.get('summary', '')}") return final def main() -> int: task = " ".join(sys.argv[1:]).strip() or DEFAULT_TASK run_demo(task) return 0 if __name__ == "__main__": raise SystemExit(main())