feat: solution for 'Повторный экзамен: Исследовательский бриф (план → шаги → сводка)'

This commit is contained in:
2026-06-29 12:13:24 +03:00
parent a94ed08df5
commit 366ec8dee7
11 changed files with 232 additions and 135 deletions
+5
View File
@@ -0,0 +1,5 @@
# OpenAI API key
OPENAI_API_KEY=your_openai_api_key_here
# Tavily API key
TAVILY_API_KEY=your_tavily_api_key_here
Submodule
+1
Submodule 2 added at 7c8d02b756
Submodule
+1
Submodule 2-3-tavily added at 08e2e01b18
Submodule 8-deep-agents-from-scratch updated: 380e236ecf...865926c001
+83 -20
View File
@@ -1,27 +1,90 @@
# Экзамен: Самокорректирующийся агент # LangGraph Research Brief Agent
Главная This project demonstrates how to build a LangGraph agent that generates a short research brief for a given topic.
Мои задания The agent:
Экзамен: Самокорректирующийся агент
EN
Экзамен: Самокорректирующийся агент
Зачёт
Версия 2
Дедлайн сдачи: 31.08.2026
В работе 1. Creates an outline of 45 research steps.
2. For each step, performs a web search (via Tavily) and writes a concise note.
3. Synthesizes all notes into a coherent brief.
Требуется доработка ## Prerequisites
В вашем репозитории не реализовано требуемое LangGraph‑агент и отсутствует зависимость langgraph, необходимая для выполнения задачи. Пожалуйста, добавьте соответствующую реализацию и обновите требования. - Python 3.10+
- A **Tavily** API key (free tier available).
- An **OpenAI** API key (or any compatible LLM provider).
Редактирование ответа ## Setup
Заполните ответ и отправьте работу на проверку преподавателю. ```bash
# Clone the repository
git clone https://github.com/your-username/langgraph-research-brief.git
cd langgraph-research-brief
Тип ответа # Create a virtual environment (optional but recommended)
Текст python -m venv venv
Ссылка source venv/bin/activate # On Windows: venv\Scripts\activate
Файлы
Ссылка ( # Install dependencies
pip install -r requirements.txt
```
Create a `.env` file in the project root based on the example:
```bash
cp .env.example .env
```
Edit `.env` and replace the placeholders with your actual keys:
```
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=your_tavily_key
```
## Running the Agent
```bash
python src/main.py
```
You should see output similar to:
```
=== Outline ===
1. Identify the security requirements for MCP integration
2. Review LangChain's authentication mechanisms
3. Evaluate secure communication protocols
4. Test the integration in a sandbox environment
5. Document best practices and compliance checks
=== Notes ===
[Step 1] ... (58 sentence note)
[Step 2] ... (58 sentence note)
...
=== Final Brief ===
...
```
## Project Structure
```
src/
├── main.py # Entry point
├── graph.py # LangGraph definition
├── nodes.py # Node implementations
├── state.py # TypedDict for state
├── .env.example # Environment variable template
requirements.txt
README.md
```
## Customization
- **Topic**: Change the `default_topic` variable in `src/main.py` to generate a brief on a different subject.
- **LLM**: Swap `ChatOpenAI` for another provider (e.g., Ollama) by adjusting the imports and initialization in `src/nodes.py`.
- **Search**: Replace `TavilySearchResults` with another search tool if desired.
## License
MIT License
+3 -1
View File
@@ -1,3 +1,5 @@
langgraph langgraph
langchain-openai langchain-openai
openai langchain-tavily
tavily-python
python-dotenv
+22 -86
View File
@@ -1,92 +1,28 @@
import json from langgraph import StateGraph
from typing import Dict, Any from src.state import BriefState
from langgraph.graph import StateGraph, END from src.nodes import outline_node, research_step_node, synthesize_node
from langchain_openai import ChatOpenAI
from .state import PlanningState
def planning(state: PlanningState) -> PlanningState: def build_graph() -> StateGraph:
"""LLM node that splits the task into 36 concrete steps.""" graph = StateGraph(BriefState)
llm = ChatOpenAI(temperature=0)
prompt = (
f"Task: {state['task']}\n\n"
"Please break this task into 3-6 concrete steps. "
"Return the steps as a numbered list or a JSON array. "
"Do not add any extra text."
)
response = llm.invoke(prompt)
text = response.content.strip()
# Try to parse JSON first # Add nodes
plan: List[str] | None = None graph.add_node("outline", outline_node)
try: graph.add_node("research_step", research_step_node)
parsed = json.loads(text) graph.add_node("synthesize", synthesize_node)
if isinstance(parsed, list):
plan = [str(item) for item in parsed]
except Exception:
pass
# Fallback: parse numbered list # Define the condition for looping research steps
if plan is None: def condition(state: BriefState):
plan = [] if state["step_index"] < len(state["outline"]):
for line in text.splitlines(): return "research_step"
line = line.strip()
if not line:
continue
# Remove leading number if present
if '.' in line:
_, rest = line.split('.', 1)
step = rest.strip()
else: else:
step = line return "synthesize"
plan.append(step)
state["plan"] = plan # Build edges
state["current_step"] = 0 graph.add_edge("outline", "research_step")
state["results"] = [] graph.add_conditional_edges("research_step", condition, {
return state "research_step": "research_step",
"synthesize": "synthesize"
})
graph.add_edge("synthesize", "__end__")
def execution(state: PlanningState) -> PlanningState: return graph.compile()
"""Execute one step of the plan."""
llm = ChatOpenAI(temperature=0)
step = state["plan"][state["current_step"]]
prompt = (
f"Task: {state['task']}\n\n"
f"You are executing step {state['current_step'] + 1} of the plan.\n\n"
f"Step: {step}\n\n"
"Provide the result of this step."
)
response = llm.invoke(prompt)
result = response.content.strip()
state["results"].append(result)
state["current_step"] += 1
return state
def should_continue(state: PlanningState) -> str:
"""Decide whether to loop back to execution or finish."""
if state["current_step"] < len(state["plan"]):
return "execute"
return "finish"
def create_graph() -> StateGraph:
graph = StateGraph(PlanningState)
graph.add_node("planning", planning)
graph.add_node("execution", execution)
graph.add_node("finish", lambda state: state)
graph.add_conditional_edges(
"planning",
lambda _: "execute",
{"execute": "execution"}
)
graph.add_conditional_edges(
"execution",
should_continue,
{"execute": "execution", "finish": "finish"}
)
graph.set_entry_point("planning")
graph.set_finish_point("finish")
return graph
+25 -21
View File
@@ -1,34 +1,38 @@
import os import os
from src.graph import create_graph from dotenv import load_dotenv
from src.state import PlanningState from src.graph import build_graph
from src.state import BriefState
def main() -> None: def main():
# Ensure the OpenAI API key is set # Load environment variables
if "OPENAI_API_KEY" not in os.environ: load_dotenv()
raise RuntimeError("Please set the OPENAI_API_KEY environment variable.") # Default topic
default_topic = "Как студенту безопасно подключать MCP к LangChain"
task = "Compare Python and JavaScript" # Initial state
initial_state: PlanningState = { initial_state: BriefState = {
"task": task, "topic": default_topic,
"plan": None, "outline": None,
"current_step": 0, "step_index": 0,
"results": [] "notes": [],
"final_brief": None
} }
graph = create_graph() # Build and run the graph
graph = build_graph()
final_state = graph.invoke(initial_state) final_state = graph.invoke(initial_state)
print("\n=== Plan ===") # Print results
for i, step in enumerate(final_state["plan"], 1): print("\n=== Outline ===")
for i, step in enumerate(final_state["outline"], 1):
print(f"{i}. {step}") print(f"{i}. {step}")
print("\n=== Results ===") print("\n=== Notes ===")
for i, res in enumerate(final_state["results"], 1): for i, note in enumerate(final_state["notes"], 1):
print(f"[Step {i}] {res}") print(f"[Step {i}] {note}\n")
print("\n=== Final Summary ===") print("\n=== Final Brief ===")
summary = "\n".join(final_state["results"]) print(final_state["final_brief"])
print(summary)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+83
View File
@@ -0,0 +1,83 @@
import os
import re
from typing import Dict, Any
from langchain_openai import ChatOpenAI
from langchain_tavily import TavilySearchResults
from src.state import BriefState
# Load API keys from environment
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
# Initialize LLM and Tavily
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2, openai_api_key=OPENAI_API_KEY)
tavily = TavilySearchResults(tavily_api_key=TAVILY_API_KEY, max_results=3)
def outline_node(state: BriefState) -> Dict[str, Any]:
"""
Generate a concise outline of 4-5 research steps for the given topic.
"""
topic = state["topic"]
prompt = (
f"Create a concise outline of 4-5 research steps for the topic: \"{topic}\".\n"
"Return the steps as a numbered list, one step per line."
)
response = llm.invoke(prompt)
text = response.content if hasattr(response, "content") else str(response)
# Extract lines that look like numbered steps
steps = re.findall(r"^\s*\d+\.\s*(.+)$", text, re.MULTILINE)
if not steps:
# Fallback: split by newlines
steps = [line.strip() for line in text.splitlines() if line.strip()]
state["outline"] = steps
state["step_index"] = 0
state["notes"] = []
return {"outline": steps, "step_index": 0, "notes": []}
def research_step_node(state: BriefState) -> Dict[str, Any]:
"""
Perform a web search for the current outline step and generate a short note.
"""
outline = state["outline"]
idx = state["step_index"]
if idx >= len(outline):
return {}
current_step = outline[idx]
# Perform web search
search_results = tavily.invoke({"query": current_step})
# Prepare a prompt for summarization
prompt = (
f"Using the following web search results, write a concise note (5-8 sentences) about the topic:\n\n"
f"{search_results}\n\n"
f"Note:\n"
)
response = llm.invoke(prompt)
note = response.content if hasattr(response, "content") else str(response)
# Append note and increment step_index
notes = state["notes"] + [note.strip()]
state["notes"] = notes
state["step_index"] = idx + 1
return {"notes": notes, "step_index": idx + 1}
def synthesize_node(state: BriefState) -> Dict[str, Any]:
"""
Combine all notes into a coherent brief with headings.
"""
outline = state["outline"]
notes = state["notes"]
combined = ""
for step, note in zip(outline, notes):
combined += f"**{step}**\n\n{note}\n\n"
prompt = (
f"Using the following sections, write a concise research brief (1 page) that summarizes the key points.\n\n"
f"{combined}\n\n"
f"Brief:\n"
)
response = llm.invoke(prompt)
final_brief = response.content if hasattr(response, "content") else str(response)
state["final_brief"] = final_brief.strip()
return {"final_brief": state["final_brief"]}
+6 -5
View File
@@ -1,7 +1,8 @@
from typing import TypedDict, List, Optional from typing import TypedDict, List, Optional
class PlanningState(TypedDict): class BriefState(TypedDict):
task: str topic: str
plan: Optional[List[str]] outline: List[str] | None
current_step: int step_index: int
results: List[str] notes: List[str]
final_brief: str | None
Submodule structured-output-union-api added at 993512bd88