Spaces:
Sleeping
Sleeping
| """Parent orchestration graph for the harmonic analysis multi-agent system.""" | |
| import json | |
| from typing import Annotated, TypedDict | |
| from langchain_core.language_models import BaseChatModel | |
| from langchain_core.messages import HumanMessage | |
| from langchain_core.tools import BaseTool | |
| from langgraph.graph import END, START, StateGraph | |
| from langgraph.graph.state import CompiledStateGraph | |
| from pydantic import BaseModel | |
| from .analysis import build_analysis_subgraph | |
| from .research import build_research_subgraph | |
| _MIN_SONGS = 10 | |
| _INITIAL_LIMIT = 10 | |
| _LIMIT_INCREMENT = 10 | |
| _MAX_LIMIT = 50 | |
| class HarmonicInput(BaseModel): | |
| user_input: str | |
| def _merge_researched(existing: list[dict], new: list[dict]) -> list[dict]: | |
| seen = {(s["title"], s["artist"]) for s in existing} | |
| return existing + [s for s in new if (s["title"], s["artist"]) not in seen] | |
| class HarmonicState(TypedDict): | |
| user_input: str | |
| limit: int | |
| originality_score: float | None | |
| neighbours: list[dict] | |
| researched_neighbours: Annotated[list[dict], _merge_researched] | |
| songs_sent_to_research: list[dict] # deterministic parent-owned log of dispatched songs | |
| songs_to_research: list[dict] # set by parent before calling research subgraph | |
| class HarmonicOutput(TypedDict): | |
| response: str | |
| def build_harmonic_graph( | |
| analysis_llm: BaseChatModel, | |
| research_llm: BaseChatModel, | |
| mcp_tools: list[BaseTool], | |
| search_tools: list[BaseTool], | |
| min_songs: int = _MIN_SONGS, | |
| presenter_llm: BaseChatModel | None = None, | |
| ) -> CompiledStateGraph: | |
| """Build the harmonic analysis → research multi-agent graph. | |
| :param analysis_llm: LLM for harmonic analysis (must support tool calling). | |
| :param research_llm: LLM for internet research (must support tool calling). | |
| :param mcp_tools: Harmonic analysis MCP tools. | |
| :param search_tools: Web search tools for research. | |
| :param min_songs: Minimum well-known songs the researcher must find before the graph ends. | |
| :param presenter_llm: LLM for generating the final summary. Defaults to analysis_llm. | |
| """ | |
| analysis_subgraph = build_analysis_subgraph(analysis_llm, mcp_tools) | |
| research_subgraph = build_research_subgraph(research_llm, search_tools) | |
| presenter_llm = presenter_llm or analysis_llm | |
| def prepare_analysis(state: HarmonicState) -> dict: | |
| return { | |
| "limit": _INITIAL_LIMIT, | |
| "originality_score": None, | |
| "neighbours": [], | |
| "researched_neighbours": [], | |
| "songs_sent_to_research": [], | |
| } | |
| def prepare_research(state: HarmonicState) -> dict: | |
| already_sent = {(s["title"], s["artist"]) for s in state["songs_sent_to_research"]} | |
| to_research = [ | |
| n for n in state["neighbours"] | |
| if (n["title"], n["artist"]) not in already_sent | |
| ] | |
| return { | |
| "songs_to_research": to_research, | |
| "songs_sent_to_research": state["songs_sent_to_research"] + to_research, | |
| } | |
| def widen(state: HarmonicState) -> dict: | |
| return { | |
| "limit": state["limit"] + _LIMIT_INCREMENT, | |
| "originality_score": None, | |
| "neighbours": [], | |
| } | |
| def check_well_known(state: HarmonicState) -> str: | |
| well_known = sum( | |
| 1 for s in state["researched_neighbours"] | |
| if s.get("chart_peak") is not None or s.get("is_famous_artist") | |
| ) | |
| if well_known >= min_songs or state["limit"] >= _MAX_LIMIT: | |
| return "present" | |
| return "widen" | |
| def present(state: HarmonicState) -> dict: | |
| similarity_by_song = {(n["title"], n["artist"]): n.get("similarity") for n in state["neighbours"]} | |
| well_known = [] | |
| for s in state["researched_neighbours"]: | |
| if s.get("chart_peak") is None and not s.get("is_famous_artist"): | |
| continue | |
| similarity = similarity_by_song.get((s["title"], s["artist"])) | |
| if similarity is None: | |
| continue | |
| well_known.append({**s, "similarity": similarity}) | |
| prompt = ( | |
| f"The chord sequence has an originality score of {state['originality_score']:.4f} " | |
| f"(0 = many harmonic matches, 1 = unique).\n\n" | |
| f"The following well-known songs were found to be harmonically similar:\n" | |
| f"{json.dumps(well_known, indent=2)}\n\n" | |
| "Write a short, readable summary for a musician. List each song with its similarity score " | |
| "and any interesting facts from the research (chart position, chart name, artist notes). " | |
| "Do not reproduce raw JSON — write in natural prose." | |
| ) | |
| response = presenter_llm.invoke([HumanMessage(content=prompt)]) | |
| return {"response": response.content} | |
| graph = StateGraph(HarmonicState, input=HarmonicInput, output=HarmonicOutput) | |
| graph.add_node("prepare_analysis", prepare_analysis) | |
| graph.add_node("analysis", analysis_subgraph) | |
| graph.add_node("prepare_research", prepare_research) | |
| graph.add_node("research", research_subgraph) | |
| graph.add_node("widen", widen) | |
| graph.add_node("present", present) | |
| graph.add_edge(START, "prepare_analysis") | |
| graph.add_edge("prepare_analysis", "analysis") | |
| graph.add_edge("analysis", "prepare_research") | |
| graph.add_edge("prepare_research", "research") | |
| graph.add_conditional_edges("research", check_well_known, ["widen", "present"]) | |
| graph.add_edge("widen", "analysis") | |
| graph.add_edge("present", END) | |
| return graph.compile() | |