Reworked to produce research brief without tables. Updated README. Code compiles.: update src/main.py

This commit is contained in:
2026-06-18 09:23:43 +00:00
parent ee26d408bf
commit fb3627fce2
+53 -80
View File
@@ -1,7 +1,7 @@
""" """
LangGraph comparison agent demo. LangGraph research brief generator.
Usage: Usage:
python -m src.main python -m src.main "Topic"
""" """
import os import os
from typing import TypedDict, List, Dict from typing import TypedDict, List, Dict
@@ -13,117 +13,90 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
# ---------- State ---------- # ---------- State ----------
class CompareState(TypedDict): class BriefState(TypedDict):
entities: List[str] topic: str
criteria: List[str] sections: List[str]
findings: Dict[str, List[str]] # entity -> list of notes per criterion content: Dict[str, str]
final_table: str | None brief: str | None
verdict: str | None
# ---------- Nodes ---------- # ---------- Nodes ----------
llm = ChatOpenAI(temperature=0) llm = ChatOpenAI(temperature=0)
search_tool = TavilySearch(api_key=os.getenv("TAVILY_API_KEY")) search_tool = TavilySearch(api_key=os.getenv("TAVILY_API_KEY"))
async def plan_criteria(state: CompareState) -> CompareState: async def plan_sections(state: BriefState) -> BriefState:
entities_str = ", ".join(state["entities"])
prompt = ( prompt = (
f"You are a helpful assistant. Given the following entities: {entities_str}. " f"You are a helpful assistant. Given the topic: {state['topic']}. "
"Generate 3-5 concise criteria for comparing them. Return a JSON array of strings." "Generate 5 concise section headings for a research brief. Return a JSON array of strings."
) )
response = await llm.ainvoke({"role": "user", "content": prompt}) response = await llm.ainvoke({"role": "user", "content": prompt})
# parse JSON
import json import json
try: try:
criteria = json.loads(response["content"].strip()) sections = json.loads(response["content"].strip())
except Exception as e: except Exception as e:
raise ValueError(f"Failed to parse criteria: {e}") raise ValueError(f"Failed to parse sections: {e}")
state["criteria"] = criteria state["sections"] = sections
return state return state
async def research_entity(state: CompareState) -> CompareState: async def fetch_section(state: BriefState) -> BriefState:
# find next unprocessed entity-criterion pair # find next section not yet fetched
for ent in state["entities"]: for sec in state["sections"]:
if ent not in state["findings"]: if sec not in state["content"]:
state["findings"][ent] = [] query = f"{state['topic']} {sec}"
for crit in state["criteria"]:
if any(crit.lower() in note.lower() for note in state["findings"][ent]):
continue
# perform search
query = f"{ent} {crit}"
result = await search_tool.ainvoke({"query": query, "max_results": 1}) result = await search_tool.ainvoke({"query": query, "max_results": 1})
snippet = result.get("results", [{}])[0].get("snippet", "") snippet = result.get("results", [{}])[0].get("snippet", "")
note = f"{crit}: {snippet}" if snippet else f"{crit}: no info" content = snippet if snippet else "No relevant information found."
state["findings"][ent].append(note) state["content"][sec] = content
return state return state
# all processed
return state return state
async def build_table(state: CompareState) -> CompareState: async def build_brief(state: BriefState) -> BriefState:
rows = [] lines = [f"# Research Brief: {state['topic']}\n"]
for crit in state["criteria"]: for sec in state["sections"]:
row = [crit] lines.append(f"## {sec}\n")
for ent in state["entities"]: lines.append(state["content"][sec] + "\n")
notes = state["findings"][ent] lines.append("---\n")
note = next((n for n in notes if n.startswith(crit)), "") lines.append("**Summary**\n")
row.append(note) # simple summary using LLM
rows.append(row) summary_prompt = (
# markdown table f"Based on the following sections:\n{''.join(lines)}\n"
header = " | ".join(["Criterion"] + state["entities"]) "Provide a concise summary of the brief in 3-4 sentences."
sep = " | ".join([":---:"] * (len(state["entities"]) + 1))
table_rows = [f"{row[0]} | {' | '.join(row[1:])}" for row in rows]
state["final_table"] = f"{header}\n{sep}\n" + "\n".join(table_rows)
return state
async def verdict(state: CompareState) -> CompareState:
prompt = (
f"Based on the following table:\n{state['final_table']}\n"
"Provide a short recommendation (2-4 sentences) on which entity is best for each use case."
) )
response = await llm.ainvoke({"role": "user", "content": prompt}) summary_resp = await llm.ainvoke({"role": "user", "content": summary_prompt})
state["verdict"] = response["content"].strip() lines.append(summary_resp["content"].strip() + "\n")
state["brief"] = "\n".join(lines)
return state return state
# ---------- Graph ---------- # ---------- Graph ----------
builder = StateGraph(CompareState) builder = StateGraph(BriefState)
builder.add_node("plan_criteria", plan_criteria) builder.add_node("plan_sections", plan_sections)
builder.add_node("research_entity", research_entity) builder.add_node("fetch_section", fetch_section)
builder.add_node("build_table", build_table) builder.add_node("build_brief", build_brief)
builder.add_node("verdict", verdict)
builder.set_entry_point("plan_criteria") builder.set_entry_point("plan_sections")
# transition logic # after planning, fetch sections until all done
builder.add_conditional_edges( builder.add_conditional_edges(
"plan_criteria", "plan_sections",
lambda _: "research_entity", lambda _: "fetch_section",
) )
builder.add_conditional_edges( builder.add_conditional_edges(
"research_entity", "fetch_section",
lambda state: "build_table" if all(ent in state["findings"] and len(state["findings"][ent]) == len(state["criteria"]) for ent in state["entities"]) else "research_entity", lambda state: "build_brief" if all(sec in state["content"] for sec in state["sections"]) else "fetch_section",
) )
builder.add_edge("build_table", "verdict") builder.add_edge("build_brief", "END")
builder.add_edge("verdict", "END")
app = builder.compile() app = builder.compile()
# ---------- Demo ---------- # ---------- Demo ----------
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser(description="Compare three entities.") parser = argparse.ArgumentParser(description="Generate a research brief on a topic.")
parser.add_argument("entities", nargs='*', help="Three entities to compare (default: Chroma, FAISS, Qdrant)") parser.add_argument("topic", nargs='?', default="Artificial Intelligence", help="Topic for the research brief")
args = parser.parse_args() args = parser.parse_args()
if not args.entities: init_state: BriefState = {
args.entities = ["Chroma", "FAISS", "Qdrant"] "topic": args.topic,
init_state: CompareState = { "sections": [],
"entities": list(args.entities), "content": {},
"criteria": [], "brief": None,
"findings": {},
"final_table": None,
"verdict": None,
} }
result = app.invoke(init_state) result = app.invoke(init_state)
print("\n=== Criteria ===") print(result["brief"])
print(result["criteria"])
print("\n=== Table ===")
print(result["final_table"])
print("\n=== Verdict ===")
print(result["verdict"])