added quick connect auth from jellyfin, still needs to have some more cleaning before push to prod
This commit is contained in:
+13
-29
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
LangGraph agent graph factory.
|
||||
|
||||
Builds a StateGraph that replaces the manual tool-calling loop in api/v1/chat.py.
|
||||
The graph has two nodes:
|
||||
Builds a StateGraph with two nodes:
|
||||
- agent_node : calls the LLM (with system prompt + tool definitions)
|
||||
- tool_node : executes tool calls via the existing skill system
|
||||
|
||||
A conditional edge routes tool_calls back to the agent, or ends the run.
|
||||
When a tool fails due to missing authentication, the failure message is
|
||||
relayed to the LLM, which tells the user to use /login.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -97,18 +98,14 @@ def _make_agent_node(
|
||||
full: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
||||
for m in messages:
|
||||
if isinstance(m, dict):
|
||||
# Already a plain dict — pass through.
|
||||
# But fix tool_calls if they're in LangChain format.
|
||||
d = dict(m)
|
||||
tc = d.get("tool_calls")
|
||||
if tc and isinstance(tc, list) and tc and isinstance(tc[0], dict) and "function" not in tc[0]:
|
||||
d["tool_calls"] = _langchain_tc_to_openai(tc)
|
||||
full.append(d)
|
||||
else:
|
||||
# LangChain message object → OpenAI-compatible dict
|
||||
role = _lc_role_to_openai(getattr(m, "type", "user"))
|
||||
d: dict[str, Any] = {"role": role, "content": getattr(m, "content", "")}
|
||||
# Serialize tool_calls back to OpenAI format (if this is an AI msg)
|
||||
tc = getattr(m, "tool_calls", None)
|
||||
if tc:
|
||||
d["tool_calls"] = _langchain_tc_to_openai(tc)
|
||||
@@ -125,7 +122,6 @@ def _make_agent_node(
|
||||
)
|
||||
choice = resp.choices[0]
|
||||
|
||||
# Convert OpenAI tool_calls to the dict format LangChain expects.
|
||||
raw_tool_calls = list(choice.message.tool_calls) if choice.message.tool_calls else []
|
||||
tool_calls: list[dict[str, Any]] = []
|
||||
for tc in raw_tool_calls:
|
||||
@@ -153,9 +149,9 @@ def _make_tool_node(skill_names: list[str]):
|
||||
"""
|
||||
Return a callable that executes tool_calls from the last AI message.
|
||||
|
||||
This replaces LangGraph's built-in ToolNode — we call our own
|
||||
`execute_tool()` pipeline so that skill-level auth, httpx sessions,
|
||||
and ToolResult handling are fully preserved.
|
||||
If a tool fails because the user isn't authenticated, the failure
|
||||
message (which tells the user to /login) is returned to the LLM.
|
||||
The LLM naturally relays the instructions to the user.
|
||||
"""
|
||||
|
||||
async def tool_node(state: AgentState) -> dict[str, list]:
|
||||
@@ -164,18 +160,16 @@ def _make_tool_node(skill_names: list[str]):
|
||||
if not tool_calls:
|
||||
return {"messages": []}
|
||||
|
||||
discord_user_id = state.get("discord_user_id")
|
||||
|
||||
results: list[ToolMessage] = []
|
||||
for tc in tool_calls:
|
||||
# Handle both LangChain format (top-level name/args) and
|
||||
# OpenAI format (nested "function" key).
|
||||
if isinstance(tc, dict):
|
||||
if "function" in tc:
|
||||
# OpenAI format: {"id":..., "function": {"name":..., "arguments":"..."}}
|
||||
fn = tc["function"]
|
||||
fn_name = fn.get("name", "")
|
||||
fn_args_raw = fn.get("arguments", "{}")
|
||||
else:
|
||||
# LangChain format: {"name":..., "args":{...}, "id":...}
|
||||
fn_name = tc.get("name", "")
|
||||
fn_args_raw = tc.get("args", {})
|
||||
tc_id = tc.get("id", "")
|
||||
@@ -184,13 +178,15 @@ def _make_tool_node(skill_names: list[str]):
|
||||
fn_args_raw = getattr(tc, "args", {})
|
||||
tc_id = getattr(tc, "id", "")
|
||||
|
||||
# Parse args if they arrive as a JSON string
|
||||
if isinstance(fn_args_raw, str):
|
||||
fn_args = json.loads(fn_args_raw)
|
||||
else:
|
||||
fn_args = fn_args_raw
|
||||
|
||||
tr = await execute_tool(skill_names, fn_name, fn_args)
|
||||
tr = await execute_tool(
|
||||
skill_names, fn_name, fn_args,
|
||||
discord_user_id=discord_user_id,
|
||||
)
|
||||
content = tr.content if tr else f"Tool '{fn_name}' is not available."
|
||||
results.append(ToolMessage(content=content, tool_call_id=tc_id))
|
||||
|
||||
@@ -224,27 +220,16 @@ def create_agent_graph(
|
||||
) -> StateGraph:
|
||||
"""
|
||||
Build and compile a LangGraph StateGraph for a single agent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
client : The OpenAI-compatible client (already authenticated).
|
||||
agent_skills : Skill names assigned to the agent (e.g. ["seerr", "triage"]).
|
||||
system_prompt : The fully-built system prompt (base + skill fragments).
|
||||
model_name : Model identifier sent to the LLM provider.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A compiled LangGraph graph ready for `.ainvoke()` or `.astream()`.
|
||||
"""
|
||||
tool_defs = get_all_tools(agent_skills)
|
||||
|
||||
graph = StateGraph(AgentState)
|
||||
|
||||
# Nodes
|
||||
graph.add_node(
|
||||
"agent_node",
|
||||
_make_agent_node(client, system_prompt, tool_defs, model_name),
|
||||
)
|
||||
|
||||
if tool_defs:
|
||||
graph.add_node("tool_node", _make_tool_node(agent_skills))
|
||||
graph.add_conditional_edges("agent_node", _should_continue, {
|
||||
@@ -253,7 +238,6 @@ def create_agent_graph(
|
||||
})
|
||||
graph.add_edge("tool_node", "agent_node")
|
||||
else:
|
||||
# No tools — agent responds once and finishes
|
||||
graph.add_edge("agent_node", END)
|
||||
|
||||
graph.set_entry_point("agent_node")
|
||||
|
||||
Reference in New Issue
Block a user