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
Главная
Мои задания
Экзамен: Самокорректирующийся агент
EN
Экзамен: Самокорректирующийся агент
Зачёт
Версия 2
Дедлайн сдачи: 31.08.2026
This project demonstrates how to build a LangGraph agent that generates a short research brief for a given topic.
The agent:
В работе
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
langchain-openai
openai
langchain-tavily
tavily-python
python-dotenv
+22 -86
View File
@@ -1,92 +1,28 @@
import json
from typing import Dict, Any
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from .state import PlanningState
from langgraph import StateGraph
from src.state import BriefState
from src.nodes import outline_node, research_step_node, synthesize_node
def planning(state: PlanningState) -> PlanningState:
"""LLM node that splits the task into 36 concrete steps."""
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()
def build_graph() -> StateGraph:
graph = StateGraph(BriefState)
# Try to parse JSON first
plan: List[str] | None = None
try:
parsed = json.loads(text)
if isinstance(parsed, list):
plan = [str(item) for item in parsed]
except Exception:
pass
# Add nodes
graph.add_node("outline", outline_node)
graph.add_node("research_step", research_step_node)
graph.add_node("synthesize", synthesize_node)
# Fallback: parse numbered list
if plan is None:
plan = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
# Remove leading number if present
if '.' in line:
_, rest = line.split('.', 1)
step = rest.strip()
# Define the condition for looping research steps
def condition(state: BriefState):
if state["step_index"] < len(state["outline"]):
return "research_step"
else:
step = line
plan.append(step)
return "synthesize"
state["plan"] = plan
state["current_step"] = 0
state["results"] = []
return state
# Build edges
graph.add_edge("outline", "research_step")
graph.add_conditional_edges("research_step", condition, {
"research_step": "research_step",
"synthesize": "synthesize"
})
graph.add_edge("synthesize", "__end__")
def execution(state: PlanningState) -> PlanningState:
"""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
return graph.compile()
+25 -21
View File
@@ -1,34 +1,38 @@
import os
from src.graph import create_graph
from src.state import PlanningState
from dotenv import load_dotenv
from src.graph import build_graph
from src.state import BriefState
def main() -> None:
# Ensure the OpenAI API key is set
if "OPENAI_API_KEY" not in os.environ:
raise RuntimeError("Please set the OPENAI_API_KEY environment variable.")
def main():
# Load environment variables
load_dotenv()
# Default topic
default_topic = "Как студенту безопасно подключать MCP к LangChain"
task = "Compare Python and JavaScript"
initial_state: PlanningState = {
"task": task,
"plan": None,
"current_step": 0,
"results": []
# Initial state
initial_state: BriefState = {
"topic": default_topic,
"outline": None,
"step_index": 0,
"notes": [],
"final_brief": None
}
graph = create_graph()
# Build and run the graph
graph = build_graph()
final_state = graph.invoke(initial_state)
print("\n=== Plan ===")
for i, step in enumerate(final_state["plan"], 1):
# Print results
print("\n=== Outline ===")
for i, step in enumerate(final_state["outline"], 1):
print(f"{i}. {step}")
print("\n=== Results ===")
for i, res in enumerate(final_state["results"], 1):
print(f"[Step {i}] {res}")
print("\n=== Notes ===")
for i, note in enumerate(final_state["notes"], 1):
print(f"[Step {i}] {note}\n")
print("\n=== Final Summary ===")
summary = "\n".join(final_state["results"])
print(summary)
print("\n=== Final Brief ===")
print(final_state["final_brief"])
if __name__ == "__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
class PlanningState(TypedDict):
task: str
plan: Optional[List[str]]
current_step: int
results: List[str]
class BriefState(TypedDict):
topic: str
outline: List[str] | None
step_index: int
notes: List[str]
final_brief: str | None
Submodule structured-output-union-api added at 993512bd88