harmonic-analysis / agents /multi /research.py
ohollo's picture
Langsmith evals
7d65f7a
Raw
History Blame Contribute Delete
3.99 kB
"""Research agent subgraph."""
from typing import Annotated, TypedDict
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import HumanMessage, RemoveMessage, SystemMessage
from langchain_core.tools import BaseTool
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt import ToolNode
from pydantic import BaseModel
_SYSTEM = (
"You are a music researcher. Search the web to find chart information for a song. "
"Make sure it's actually the song by the artist in question, not a song of the same name "
"by another artist. Form queries like: '\"Let It Be\" Beatles UK Singles Chart peak position'."
)
class ResearchState(TypedDict):
songs_to_research: list[dict] # received from parent
current_song_index: int # internal loop counter
researched_neighbours: list[dict] # returned to parent (merged by parent reducer)
messages: Annotated[list, add_messages] # private, reset per song
class _ResearchedSong(BaseModel):
title: str
artist: str
chart_peak: int | None = None
chart_name: str | None = None
is_famous_artist: bool | None = None
notes: str | None = None
def build_research_subgraph(
research_llm: BaseChatModel,
search_tools: list[BaseTool],
) -> CompiledStateGraph:
"""Build the music research agent subgraph.
:param research_llm: LLM for internet research (must support tool calling).
:param search_tools: Web search tools for research.
"""
research_llm_with_tools = research_llm.bind_tools(search_tools)
def start(state: ResearchState) -> dict:
return {"researched_neighbours": [], "current_song_index": 0}
def prepare_song(state: ResearchState) -> dict:
song = state["songs_to_research"][state["current_song_index"]]
clear = [RemoveMessage(id=m.id) for m in state["messages"]]
return {
"messages": clear + [
SystemMessage(content=_SYSTEM),
HumanMessage(content=f"Research \"{song['title']}\" by {song['artist']}."),
]
}
def agent(state: ResearchState) -> dict:
return {"messages": [research_llm_with_tools.invoke(state["messages"])]}
def router(state: ResearchState) -> str:
last = state["messages"][-1]
return "tools" if getattr(last, "tool_calls", None) else "extract_song"
def extract_song(state: ResearchState) -> dict:
song = state["songs_to_research"][state["current_song_index"]]
structured = research_llm.with_structured_output(_ResearchedSong)
prompt = HumanMessage(content=(
f"Summarise your findings for \"{song['title']}\" by {song['artist']}. "
"If it charted, give the peak position and chart name. "
"If not, note whether the artist is well-known."
))
result: _ResearchedSong = structured.invoke(state["messages"] + [prompt])
return {
"researched_neighbours": state["researched_neighbours"] + [result.model_dump()],
"current_song_index": state["current_song_index"] + 1,
}
def route_next(state: ResearchState) -> str:
return "prepare_song" if state["current_song_index"] < len(state["songs_to_research"]) else END
graph = StateGraph(ResearchState)
graph.add_node("start", start)
graph.add_node("prepare_song", prepare_song)
graph.add_node("agent", agent)
graph.add_node("tools", ToolNode(search_tools))
graph.add_node("extract_song", extract_song)
graph.add_edge(START, "start")
graph.add_edge("start", "prepare_song")
graph.add_edge("prepare_song", "agent")
graph.add_conditional_edges("agent", router, ["tools", "extract_song"])
graph.add_edge("tools", "agent")
graph.add_conditional_edges("extract_song", route_next, ["prepare_song", END])
return graph.compile()