add: main.py

This commit is contained in:
2026-06-04 16:28:11 +00:00
parent 50ef6ecf6a
commit 9636a1f98e
+79 -84
View File
@@ -1,14 +1,20 @@
import os import os
import asyncio import asyncio
from typing import TypedDict, Annotated, List from typing import TypedDict, Annotated, List
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langchain.tools import tool from langchain.tools import tool
from deepagents import create_deep_agent from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_tavily import TavilySearchResults
# --- LLM configuration (OpenRouter) --- # Load environment variables
from dotenv import load_dotenv
load_dotenv()
# LLM configuration OpenRouter
llm = ChatOpenAI( llm = ChatOpenAI(
model="openai/gpt-oss-20b:free", model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1", base_url="https://openrouter.ai/api/v1",
@@ -16,26 +22,22 @@ llm = ChatOpenAI(
temperature=0.0, temperature=0.0,
) )
# --- Backend for deepagents --- # Backend for deepagents local shell + filesystem
backend = CompositeBackend([ backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"), LocalShellBackend(workspace_dir="./workspace"),
FilesystemBackend(), FilesystemBackend(),
]) ])
# --- Tavily search tool --- # Tavily search tool real web search
@tool @tool
def tavily_search(query: str) -> str: def tavily_search(query: str) -> str:
"""Search the web using Tavily and return a short summary of the top result.""" """Search the web using Tavily and return a short summary."""
from tavily import TavilyClient tavily = TavilySearchResults(max_results=3)
client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) results = tavily.run(query)
results = client.search(query, max_results=1) # Concatenate titles and snippets
if not results: return "\n".join(f"{r['title']}: {r['content']}" for r in results)
return "No relevant information found."
# Return the first result's content (title + snippet)
first = results[0]
return f"{first.title}\n{first.snippet}"
# --- State definition for LangGraph (used inside a tool) --- # State definition
class BriefState(TypedDict): class BriefState(TypedDict):
topic: str topic: str
outline: List[str] | None outline: List[str] | None
@@ -43,110 +45,103 @@ class BriefState(TypedDict):
notes: List[str] notes: List[str]
final_brief: str | None final_brief: str | None
# --- LangGraph graph implementation ---
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# Helper to format outline as a numbered list
def format_outline(outline: List[str]) -> str:
return "\n".join(f"{i+1}. {p}" for i, p in enumerate(outline))
# Node: generate outline # Node: generate outline
async def outline_node(state: BriefState) -> BriefState: async def outline_node(state: BriefState) -> BriefState:
prompt = f"Create a concise 45 point outline for a research brief on the topic: {state['topic']}" prompt = (
f"Generate a concise outline of 45 research steps for the topic: {state['topic']}\n"
"Return a JSON array of strings, each a single step."
)
response = await llm.ainvoke([HumanMessage(content=prompt)]) response = await llm.ainvoke([HumanMessage(content=prompt)])
outline_text = response.content.strip() # Extract JSON array
# Assume the LLM returns a numbered list; split into lines import json, re
outline = [line.strip() for line in outline_text.splitlines() if line.strip()] try:
state['outline'] = outline array_text = re.search(r"\[.*\]", response.content, re.S).group(0)
state['step_index'] = 0 outline = json.loads(array_text)
state['notes'] = [] except Exception:
outline = ["Step 1: ...", "Step 2: ...", "Step 3: ...", "Step 4: ..."]
state["outline"] = outline
state["step_index"] = 0
state["notes"] = []
return state return state
# Node: research one step # Node: research one step
async def research_step_node(state: BriefState) -> BriefState: async def research_step_node(state: BriefState) -> BriefState:
current_point = state['outline'][state['step_index']] step = state["outline"][state["step_index"]]
# Use Tavily to get info # Use Tavily to gather info
search_query = f"{current_point}" search_query = f"{state['topic']} {step}"
search_result = tavily_search(search_query) search_result = tavily_search(search_query)
# Summarize the result with LLM # Summarize with LLM
prompt = f"Summarize the following information in 58 sentences, focusing on the key points relevant to the research brief: {search_result}" prompt = (
summary = await llm.ainvoke([HumanMessage(content=prompt)]) f"Using the following search results, write a concise note (58 sentences) for the step: {step}\n"
state['notes'].append(f"**{current_point}**\n{summary.content.strip()}") f"Search results:\n{search_result}\n"
state['step_index'] += 1 "Note: keep it factual and cite sources if possible."
)
note = await llm.ainvoke([HumanMessage(content=prompt)])
state["notes"].append(note.content.strip())
state["step_index"] += 1
return state return state
# Node: synthesize final brief # Node: synthesize final brief
async def synthesize_node(state: BriefState) -> BriefState: async def synthesize_node(state: BriefState) -> BriefState:
notes_text = "\n\n".join(state['notes']) notes = state["notes"]
prompt = f"Using the following notes, write a cohesive ½–1 page research brief. Include headings for each section.\n\n{notes_text}" outline = state["outline"]
prompt = (
"You are an academic writer. Using the following outline and notes, produce a cohesive research brief of ½–1 page.\n"
f"Outline: {outline}\n"
f"Notes: {notes}\n"
"Structure the brief with headings matching the outline steps."
)
brief = await llm.ainvoke([HumanMessage(content=prompt)]) brief = await llm.ainvoke([HumanMessage(content=prompt)])
state['final_brief'] = brief.content.strip() state["final_brief"] = brief.content.strip()
return state return state
# Build the graph # Build LangGraph
graph = StateGraph(BriefState) graph = StateGraph(BriefState)
graph.add_node("outline", outline_node) graph.add_node("outline", outline_node)
graph.add_node("research_step", research_step_node) graph.add_node("research_step", research_step_node)
graph.add_node("synthesize", synthesize_node) graph.add_node("synthesize", synthesize_node)
# Define transitions # Entry point
graph.set_entry_point("outline") graph.set_entry_point("outline")
# After outline, loop research_step until all steps processed
graph.add_conditional_edges( graph.add_conditional_edges(
"outline", "outline",
lambda _: "research_step", lambda _: "research_step",
) )
# After each research_step, decide whether to continue or synthesize
def research_cond(state: BriefState): graph.add_conditional_edges(
return "research_step" if state['step_index'] < len(state['outline']) else "synthesize" "research_step",
lambda state: "synthesize" if state["step_index"] >= len(state["outline"]) else "research_step",
graph.add_conditional_edges("research_step", research_cond) )
# Final node
graph.add_edge("synthesize", END) graph.add_edge("synthesize", END)
# Compile the graph into a tool app = graph.compile()
from langgraph.prebuilt import create_react_agent
# The graph will be used as a tool inside deepagents # DeepAgent wrapper
@tool
async def run_brief(topic: str) -> str:
"""Generate a research brief for the given topic."""
# Initialize state
state: BriefState = {
"topic": topic,
"outline": None,
"step_index": 0,
"notes": [],
"final_brief": None,
}
# Run the graph
async for partial_state in graph.astream(state):
pass # we just wait for completion
# After completion, return the brief
return state['final_brief']
# --- DeepAgent setup ---
agent = create_deep_agent( agent = create_deep_agent(
model=llm, model=llm,
tools=[tavily_search, run_brief], tools=[tavily_search],
backend=backend, backend=backend,
system_prompt="You are a research assistant that creates concise research briefs. Use the provided tools to gather information and synthesize a brief.", system_prompt="You are a research assistant that builds a brief.",
) )
# --- Demo execution ---
async def main(): async def main():
default_topic = "Как студенту безопасно подключать MCP к LangChain" # Default topic
result = await agent.ainvoke( topic = os.getenv("DEFAULT_TOPIC", "Как студенту безопасно подключать MCP к LangChain")
{"messages": [HumanMessage(content=f"Create a research brief on: {default_topic}")]}, # Run LangGraph to get outline and notes
{"configurable": {"thread_id": "session-1"}}, state = await app.ainvoke({"topic": topic, "outline": None, "step_index": 0, "notes": [], "final_brief": None})
) # Print outline
print("\n--- Outline ---") print("\n=== Outline ===")
# The outline is part of the first tool call; extract it for i, step in enumerate(state["outline"], 1):
for msg in result["messages"]: print(f"{i}. {step}")
if msg.type == "tool": # Print notes per step
if "outline" in msg.content.lower(): print("\n=== Notes ===")
print(msg.content) for i, note in enumerate(state["notes"], 1):
print("\n--- Final Brief ---") print(f"[Step {i}] {note}\n")
print(result["messages"][-1].content) # Print final brief
print("\n=== Final Brief ===")
print(state["final_brief"])
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())