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
|
||||||
|
|
||||||
Главная
|
This project demonstrates how to build a LangGraph agent that generates a short research brief for a given topic.
|
||||||
Мои задания
|
The agent:
|
||||||
Экзамен: Самокорректирующийся агент
|
|
||||||
5Д
|
|
||||||
EN
|
|
||||||
Экзамен: Самокорректирующийся агент
|
|
||||||
Зачёт
|
|
||||||
Версия 2
|
|
||||||
Дедлайн сдачи: 31.08.2026
|
|
||||||
|
|
||||||
В работе
|
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
|
langgraph
|
||||||
langchain-openai
|
langchain-openai
|
||||||
openai
|
langchain-tavily
|
||||||
|
tavily-python
|
||||||
|
python-dotenv
|
||||||
+22
-86
@@ -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 3‑6 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
@@ -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()
|
||||||
@@ -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
|
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
+1
Submodule structured-output-union-api added at 993512bd88
Reference in New Issue
Block a user