feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой'

This commit is contained in:
2026-06-30 00:30:10 +03:00
commit 976ed2b0b9
6 changed files with 240 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.env
dist/
build/
*.log
+65
View File
@@ -0,0 +1,65 @@
# LangGraph Reflection Demo
This project demonstrates a simple LangGraph agent that:
1. Generates a short answer (510 sentences) to a usersupplied 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.
- **LLMbased critic** that returns a verdict (`ok` or `needs_revision`) and 23 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
+3
View File
@@ -0,0 +1,3 @@
langgraph
langchain-openai
langchain-ollama
+28
View File
@@ -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
+59
View File
@@ -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()
+80
View File
@@ -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 (510 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 23 critique points.\n"
"Question: {question}\n"
"Answer: {draft}\n"
"Respond in the following format:\n"
"verdict: <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 510 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}