From 976ed2b0b929fad62ece5f9e768e376a89daa356 Mon Sep 17 00:00:00 2001 From: kuzakhmetovartur Date: Tue, 30 Jun 2026 00:30:10 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20solution=20for=20'=D0=9F=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD:=20=D0=93=D1=80=D0=B0=D1=84=20=D1=81=20?= =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=BB=D0=B5=D0=BA=D1=81=D0=B8=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +++ README.md | 65 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ src/graph.py | 28 +++++++++++++++++ src/main.py | 59 +++++++++++++++++++++++++++++++++++ src/nodes.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 240 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 src/graph.py create mode 100644 src/main.py create mode 100644 src/nodes.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b16538b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dist/ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..b23889d --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# LangGraph Reflection Demo + +This project demonstrates a simple LangGraph agent that: +1. Generates a short answer (5–10 sentences) to a user‑supplied question. +2. Critiques the answer for completeness, concreteness, and fluff. +3. If the critique indicates `needs_revision`, rewrites the answer up to a maximum number of rounds. + +## Features + +- **Separate nodes** for drafting, reflecting, and rewriting. +- **LLM‑based critic** that returns a verdict (`ok` or `needs_revision`) and 2–3 critique points. +- **Controlled loop**: rewrites only if the verdict is `needs_revision` and the round count is below `max_rounds`. +- **CLI interface**: pass a question via `-q` or input interactively. +- **Configurable maximum rounds** via `-m` (default 2). + +## Requirements + +- Python 3.10+ +- `langgraph` +- `langchain-openai` + +Install dependencies: + +```bash +pip install -r requirements.txt +``` + +## Usage + +1. **Set your OpenAI API key**: + +```bash +export OPENAI_API_KEY="your_api_key_here" +``` + +2. **Run the demo**: + +```bash +python src/main.py -q "Explain the difference between a tool and a resource in MCP." +``` + +Or simply: + +```bash +python src/main.py +``` + +and enter the question when prompted. + +The script will output the final answer, the number of rounds performed, the verdict, and the critique points. + +## Project Structure + +``` +src/ +├── main.py # CLI entry point +├── graph.py # LangGraph definition +└── nodes.py # Node implementations +requirements.txt +README.md +``` + +## License + +MIT License \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..67cdf75 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +langgraph +langchain-openai +langchain-ollama \ No newline at end of file diff --git a/src/graph.py b/src/graph.py new file mode 100644 index 0000000..7fc8ff9 --- /dev/null +++ b/src/graph.py @@ -0,0 +1,28 @@ +from typing import Dict, Any +from langgraph.graph import StateGraph +from src.nodes import ReflectState, draft_answer, reflect, rewrite + +def build_graph() -> StateGraph: + graph = StateGraph(ReflectState) + + # Add nodes + graph.add_node("draft_answer", draft_answer) + graph.add_node("reflect", reflect) + graph.add_node("rewrite", rewrite) + + # Define transitions + graph.set_entry_point("draft_answer") + graph.add_edge("draft_answer", "reflect") + + # Conditional edge after reflect + def decide_next(state: ReflectState) -> str: + if state["verdict"] == "ok": + return "end" + if state["round"] < state["max_rounds"]: + return "rewrite" + return "end" + + graph.add_conditional_edges("reflect", decide_next, {"rewrite": "rewrite", "end": "end"}) + graph.add_edge("rewrite", "reflect") + + return graph \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..005656f --- /dev/null +++ b/src/main.py @@ -0,0 +1,59 @@ +import os +import argparse +from src.graph import build_graph +from src.nodes import ReflectState + +def main(): + parser = argparse.ArgumentParser(description="LangGraph reflection demo") + parser.add_argument( + "-q", + "--question", + type=str, + help="The question to answer", + ) + parser.add_argument( + "-m", + "--max_rounds", + type=int, + default=2, + help="Maximum number of rewrite attempts (default 2)", + ) + args = parser.parse_args() + + if not args.question: + args.question = input("Enter the question: ").strip() + if not args.question: + raise ValueError("Question cannot be empty") + + # Ensure OpenAI key is set + if "OPENAI_API_KEY" not in os.environ: + raise EnvironmentError( + "OPENAI_API_KEY environment variable not set. " + "Please set it before running the script." + ) + + # Initial state + state: ReflectState = { + "question": args.question, + "draft": "", + "critique": "", + "verdict": "", + "round": 0, + "max_rounds": args.max_rounds, + } + + graph = build_graph() + compiled = graph.compile() + final_state = compiled.invoke(state) + + print("\n=== Final Result ===") + print(f"Question: {final_state['question']}") + print(f"Round: {final_state['round']}") + print(f"Verdict: {final_state['verdict']}") + print("\nCritique:") + print(final_state["critique"]) + print("\nAnswer:") + print(final_state["draft"]) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/nodes.py b/src/nodes.py new file mode 100644 index 0000000..eb18af9 --- /dev/null +++ b/src/nodes.py @@ -0,0 +1,80 @@ +from typing import TypedDict, Dict, Any +from langchain_openai import ChatOpenAI +from langchain.prompts import PromptTemplate + +# Define the state structure +class ReflectState(TypedDict): + question: str + draft: str + critique: str + verdict: str # "ok" or "needs_revision" + round: int + max_rounds: int + +# Initialize the LLM (requires OPENAI_API_KEY environment variable) +llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.2) + +# Prompt templates +DRAFT_PROMPT = PromptTemplate( + input_variables=["question"], + template=( + "You are an expert tutor. Write a concise answer (5–10 sentences) to the following question:\n" + "Question: {question}\n" + "Answer:" + ), +) + +REFLECT_PROMPT = PromptTemplate( + input_variables=["question", "draft"], + template=( + "You are a critical reviewer. Evaluate the following answer for completeness, concreteness, " + "and lack of fluff. Provide a verdict ('ok' or 'needs_revision') and 2–3 critique points.\n" + "Question: {question}\n" + "Answer: {draft}\n" + "Respond in the following format:\n" + "verdict: \n" + "critique:\n" + "- point 1\n" + "- point 2\n" + "- point 3" + ), +) + +REWRITE_PROMPT = PromptTemplate( + input_variables=["draft", "critique"], + template=( + "Rewrite the following answer to address the critique points below. " + "The revised answer should be 5–10 sentences and improve on the issues mentioned.\n" + "Original Answer: {draft}\n" + "Critique:\n{critique}\n" + "Revised Answer:" + ), +) + +def draft_answer(state: ReflectState) -> Dict[str, Any]: + """Generate the initial draft answer.""" + question = state["question"] + response = llm.invoke(DRAFT_PROMPT.format(question=question)) + draft = response.content.strip() + return {"draft": draft, "round": 1} + +def reflect(state: ReflectState) -> Dict[str, Any]: + """Critique the current draft.""" + question = state["question"] + draft = state["draft"] + response = llm.invoke(REFLECT_PROMPT.format(question=question, draft=draft)) + text = response.content.strip() + # Parse verdict and critique + verdict_line, critique_section = text.split("critique:", 1) + verdict = verdict_line.replace("verdict:", "").strip().lower() + critique = critique_section.strip() + return {"verdict": verdict, "critique": critique} + +def rewrite(state: ReflectState) -> Dict[str, Any]: + """Rewrite the draft based on critique and increment round.""" + draft = state["draft"] + critique = state["critique"] + response = llm.invoke(REWRITE_PROMPT.format(draft=draft, critique=critique)) + new_draft = response.content.strip() + new_round = state["round"] + 1 + return {"draft": new_draft, "round": new_round} \ No newline at end of file