feat: solution for 'Повторный экзамен: Исследовательский бриф (план → шаги → сводка)'
This commit is contained in:
@@ -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
@@ -1,27 +1,90 @@
|
||||
# Экзамен: Самокорректирующийся агент
|
||||
# LangGraph Research Brief Agent
|
||||
|
||||
Главная
|
||||
Мои задания
|
||||
Экзамен: Самокорректирующийся агент
|
||||
5Д
|
||||
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 4‑5 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] ... (5‑8 sentence note)
|
||||
[Step 2] ... (5‑8 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
@@ -1,3 +1,5 @@
|
||||
langgraph
|
||||
langchain-openai
|
||||
openai
|
||||
langchain-tavily
|
||||
tavily-python
|
||||
python-dotenv
|
||||
+23
-87
@@ -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 3‑6 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()
|
||||
else:
|
||||
step = line
|
||||
plan.append(step)
|
||||
# Define the condition for looping research steps
|
||||
def condition(state: BriefState):
|
||||
if state["step_index"] < len(state["outline"]):
|
||||
return "research_step"
|
||||
else:
|
||||
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
@@ -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()
|
||||
@@ -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
@@ -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
+1
Submodule structured-output-union-api added at 993512bd88
Reference in New Issue
Block a user