Add src/brief.py
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langchain_tavily import TavilySearch
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables for OpenAI and Tavily
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# State definition
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class BriefState(dict):
|
||||||
|
"""Typed dictionary representing the agent state.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
topic: str
|
||||||
|
The research topic supplied by the user.
|
||||||
|
outline: List[str] | None
|
||||||
|
4–5 point outline generated by the LLM.
|
||||||
|
step_index: int
|
||||||
|
Current index in the outline being processed.
|
||||||
|
notes: List[str]
|
||||||
|
Collected notes for each outline point.
|
||||||
|
final_brief: str | None
|
||||||
|
The synthesized brief.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
# Ensure all keys exist for type safety
|
||||||
|
self.setdefault("topic", "")
|
||||||
|
self.setdefault("outline", None)
|
||||||
|
self.setdefault("step_index", 0)
|
||||||
|
self.setdefault("notes", [])
|
||||||
|
self.setdefault("final_brief", None)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Node implementations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def outline_node(state: BriefState) -> BriefState:
|
||||||
|
"""Generate a concise outline for the topic.
|
||||||
|
|
||||||
|
The LLM is instructed to return a JSON array of strings. The output
|
||||||
|
is parsed and stored in ``state['outline']``.
|
||||||
|
"""
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
prompt = (
|
||||||
|
f"Create a concise 4‑5 point outline for a research brief on the topic: "
|
||||||
|
f"{state['topic']}. Return a JSON array of strings."
|
||||||
|
)
|
||||||
|
response = llm.invoke([HumanMessage(content=prompt)])
|
||||||
|
text = response.content if hasattr(response, "content") else str(response)
|
||||||
|
text = text.strip()
|
||||||
|
# Try to parse JSON array
|
||||||
|
try:
|
||||||
|
outline: List[str] = json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: extract first JSON array from the text
|
||||||
|
m = re.search(r"\[.*\]", text, re.S)
|
||||||
|
outline = json.loads(m.group(0)) if m else []
|
||||||
|
state["outline"] = outline
|
||||||
|
state["step_index"] = 0
|
||||||
|
state["notes"] = []
|
||||||
|
print("\n[Outline] Generated outline:")
|
||||||
|
for i, point in enumerate(outline, 1):
|
||||||
|
print(f"{i}. {point}")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def research_step_node(state: BriefState) -> BriefState:
|
||||||
|
"""Search the web for the current outline point and produce a note.
|
||||||
|
|
||||||
|
The node performs a single Tavily search and then asks the LLM to
|
||||||
|
summarize the results into a short note (5‑8 sentences).
|
||||||
|
"""
|
||||||
|
if state["step_index"] >= len(state["outline"]):
|
||||||
|
return state
|
||||||
|
point = state["outline"][state["step_index"]]
|
||||||
|
print(f"\n[Research Step {state['step_index'] + 1}] Searching for: {point}")
|
||||||
|
tavily = TavilySearch()
|
||||||
|
search_results = tavily.invoke({"query": point, "max_results": 3})
|
||||||
|
llm = ChatOpenAI()
|
||||||
|
summary_prompt = (
|
||||||
|
f"Summarize the following search results into a concise note (5‑8 sentences) for the research brief:\n{search_results}"
|
||||||
|
)
|
||||||
|
summary = llm.invoke([HumanMessage(content=summary_prompt)])
|
||||||
|
note = summary.content if hasattr(summary, "content") else str(summary)
|
||||||
|
note = note.strip()
|
||||||
|
state["notes"].append(f"• {note}")
|
||||||
|
state["step_index"] += 1
|
||||||
|
print(f"[Note] {note[:80]}...")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def synthesize_node(state: BriefState) -> BriefState:
|
||||||
|
"""Combine all collected notes into the final brief."""
|
||||||
|
print("\n[Synthesize] Combining notes into final brief.")
|
||||||
|
brief = f"Research Brief on {state['topic']}:\n\n" + "\n".join(state["notes"])
|
||||||
|
state["final_brief"] = brief
|
||||||
|
print("\n[Final Brief]\n" + brief)
|
||||||
|
return state
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Graph construction
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def build_graph(max_rounds: int = 2) -> StateGraph:
|
||||||
|
"""Return a LangGraph that implements the research brief workflow.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
max_rounds: int
|
||||||
|
Maximum number of research steps (unused here but kept for
|
||||||
|
compatibility with the original task description).
|
||||||
|
"""
|
||||||
|
graph = StateGraph(BriefState)
|
||||||
|
graph.add_node("outline", outline_node)
|
||||||
|
graph.add_node("research", research_step_node)
|
||||||
|
graph.add_node("synthesize", synthesize_node)
|
||||||
|
graph.set_entry_point("outline")
|
||||||
|
graph.add_conditional_edges(
|
||||||
|
"outline",
|
||||||
|
lambda state: "research" if state["outline"] else "synthesize",
|
||||||
|
)
|
||||||
|
graph.add_conditional_edges(
|
||||||
|
"research",
|
||||||
|
lambda state: "research" if state["step_index"] < len(state["outline"]) else "synthesize",
|
||||||
|
)
|
||||||
|
graph.add_edge("synthesize", END)
|
||||||
|
return graph
|
||||||
|
|
||||||
|
# Export for external use
|
||||||
|
__all__ = ["BriefState", "build_graph"]
|
||||||
Reference in New Issue
Block a user