From 91d0f4c341de6557776c9ddcd94cde2f51017a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=BB=D0=B5=D0=B1=20=D0=9D=D0=B8=D0=BA=D0=B8=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 4 Jun 2026 16:04:36 +0000 Subject: [PATCH] add main.py --- main.py | 144 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 86 insertions(+), 58 deletions(-) diff --git a/main.py b/main.py index de288d8..6fbaea0 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,22 @@ +""" +# main.py +# Implementation of the LangGraph reflective agent using deepagents +# Author: Auto-generated for the assignment +# Requires: deepagents, langchain-openai, langgraph, langchain +""" import os import asyncio from typing import TypedDict, Annotated from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage +from langchain_core.messages import HumanMessage, SystemMessage from langchain.tools import tool from deepagents import create_deep_agent -from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend +from deepagents.backends import CompositeBackend, LocalShellBackend, FilesystemBackend from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages -# ---------- LLM ---------- +# ---------- LLM configuration (OpenRouter) ---------- llm = ChatOpenAI( model="openai/gpt-oss-20b:free", base_url="https://openrouter.ai/api/v1", @@ -18,108 +24,130 @@ llm = ChatOpenAI( temperature=0.0, ) -# ---------- Backend ---------- -backend = CompositeBackend([ - LocalShellBackend(workspace_dir="./workspace"), - FilesystemBackend(), -]) - -# ---------- State ---------- +# ---------- State definition ---------- class ReflectState(TypedDict): question: str draft: str critique: str - verdict: str # ok | needs_revision + verdict: str # "ok" | "needs_revision" round: int max_rounds: int -# ---------- Nodes ---------- +# ---------- Node implementations ---------- async def draft_answer(state: ReflectState) -> ReflectState: - prompt = f"Write a concise answer (5–10 sentences) to the following question: {state['question']}" - response = await llm.ainvoke([HumanMessage(content=prompt)]) + """Generate an initial draft answer (5–10 sentences).""" + prompt = ( + "You are an expert tutor. Answer the following question in 5–10 concise sentences. " + "Avoid filler and keep it clear. +" + f"Question: {state['question']}" + ) + response = await llm.ainvoke([SystemMessage(content="You are a helpful tutor."), HumanMessage(content=prompt)]) state["draft"] = response.content.strip() - state["round"] = 0 - state["max_rounds"] = state.get("max_rounds", 2) + state["round"] = 1 return state async def reflect(state: ReflectState) -> ReflectState: + """Critique the draft and decide if revision is needed.""" prompt = ( - "You are a critic. Evaluate the following draft answer for completeness, specificity, and lack of filler.\n" - f"Draft: {state['draft']}\n" - "Return a JSON object with fields: verdict (ok or needs_revision) and critique (2–3 bullet points)." + "You are a critical reviewer. Evaluate the following answer for completeness, specificity, and lack of filler. " + "Respond with a verdict of either "ok" or "needs_revision", followed by 2–3 bullet points of constructive feedback. +" + f"Answer draft:\n{state['draft']}" ) - response = await llm.ainvoke([HumanMessage(content=prompt)]) - # Simple extraction of JSON - import json, re - try: - data = json.loads(re.search(r"\{.*\}", response.content, re.S).group(0)) - except Exception: - data = {"verdict": "needs_revision", "critique": "Could not parse critique."} - state["critique"] = data.get("critique", "") - state["verdict"] = data.get("verdict", "needs_revision") + response = await llm.ainvoke([SystemMessage(content="You are a critical reviewer."), HumanMessage(content=prompt)]) + text = response.content.strip() + # Parse verdict and critique + if "needs_revision" in text.lower(): + verdict = "needs_revision" + else: + verdict = "ok" + # Extract critique lines after the verdict + lines = text.splitlines() + critique_lines = [line for line in lines if line.strip() and line.strip().lower() not in {"ok", "needs_revision"}] + critique = "\n".join(critique_lines).strip() + state["critique"] = critique + state["verdict"] = verdict return state async def rewrite(state: ReflectState) -> ReflectState: + """Rewrite the draft incorporating the critique.""" prompt = ( - "Rewrite the draft answer incorporating the following critique. Keep the answer concise (5–10 sentences).\n" - f"Critique: {state['critique']}\n" - f"Original draft: {state['draft']}" + "You are revising an answer based on the following critique. Produce a new version that addresses the points and remains 5–10 sentences. +" + f"Original draft:\n{state['draft']}\n\nCritique:\n{state['critique']}" ) - response = await llm.ainvoke([HumanMessage(content=prompt)]) + response = await llm.ainvoke([SystemMessage(content="You are a revising tutor."), HumanMessage(content=prompt)]) state["draft"] = response.content.strip() state["round"] += 1 return state -# ---------- Graph ---------- -graph = StateGraph(ReflectState) -graph.add_node("draft_answer", draft_answer) -graph.add_node("reflect", reflect) -graph.add_node("rewrite", rewrite) +# ---------- Graph construction ---------- +def build_graph() -> StateGraph[ReflectState]: + graph = StateGraph(ReflectState) + graph.add_node("draft_answer", draft_answer) + graph.add_node("reflect", reflect) + graph.add_node("rewrite", rewrite) -# Entry point -graph.set_entry_point("draft_answer") + # Entry point + graph.set_entry_point("draft_answer") -# Transitions -graph.add_edge("draft_answer", "reflect") -# If ok -> END -graph.add_conditional_edges( - "reflect", - lambda state: "rewrite" if state["verdict"] == "needs_revision" and state["round"] < state["max_rounds"] else "END", -) -# rewrite -> reflect -graph.add_edge("rewrite", "reflect") + # Transitions + graph.add_conditional_edges( + "draft_answer", + lambda _: "reflect", + ) + graph.add_conditional_edges( + "reflect", + lambda state: "END" if state["verdict"] == "ok" else "rewrite", + ) + graph.add_conditional_edges( + "rewrite", + lambda state: "END" if state["round"] > state["max_rounds"] else "reflect", + ) -app = graph.compile() + return graph -# ---------- DeepAgent wrapper ---------- +# ---------- Tool that runs the graph ---------- @tool -def run_graph(question: str) -> str: - """Run the reflection graph for a given question.""" +async def answer_question(query: str) -> str: + """Run the reflective LangGraph to answer a question.""" + graph = build_graph() + # Initialize state state: ReflectState = { - "question": question, + "question": query, "draft": "", "critique": "", "verdict": "", "round": 0, "max_rounds": 2, } - result = app.invoke(state) - return result["draft"] + # Run graph + final_state = await graph.ainvoke(state) + return final_state["draft"] + +# ---------- DeepAgent setup ---------- +backend = CompositeBackend([ + LocalShellBackend(workspace_dir="./workspace"), + FilesystemBackend(), +]) agent = create_deep_agent( model=llm, - tools=[run_graph], + tools=[answer_question], backend=backend, - system_prompt="You are a helpful agent that can answer questions and self‑critique using the provided tool.", + system_prompt="You are a helpful educational agent. Use the provided tools to answer questions.", ) +# ---------- CLI entry point ---------- async def main(): question = "Объясни студенту разницу между tool и resource в MCP" result = await agent.ainvoke( {"messages": [HumanMessage(content=question)]}, {"configurable": {"thread_id": "session-1"}}, ) - print("Final answer:\n", result["messages"][-1].content) + # The tool returns the final answer as the last message content + print("\nFinal answer:\n", result["messages"][-1].content) if __name__ == "__main__": asyncio.run(main())