Spaces:
Sleeping
Sleeping
File size: 5,544 Bytes
9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a b5b19b9 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a b5b19b9 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 7d65f7a 9482535 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """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()
|