tek Atrust
chore(deploy): build monolithic server for Hugging Face
d5ef46f
Raw
History Blame Contribute Delete
11.1 kB
from __future__ import annotations
import logging
import os
from typing import Any
from pydantic_ai import Agent
from pydantic_graph import BaseNode, End, GraphRunContext
from .state import SharedState, SupervisorDecision
from .tools import propose_code_fix, read_code_file
from .utils import PAI_V1, _accumulate_usage, _build_pruned_history, _get_output, _run_agent_with_retry
logger = logging.getLogger(__name__)
# --- 2. Supervisor Node (The Brain) ---
class SupervisorNode(BaseNode[SharedState, None, str]):
async def run(
self, ctx: GraphRunContext[SharedState]
) -> MarketBotNode | LibrarianNode | SummaryNode | DevBotNode | DavidNode | End[str]:
ctx.state.step_count += 1
logger.info(f"🕸️ [Supervisor] Step {ctx.state.step_count}/{ctx.state.max_steps}")
if ctx.state.step_count > ctx.state.max_steps:
logger.warning("🚫 [Supervisor] Max recursion reached. Tripping circuit breaker.")
ctx.state.final_result = "Circuit Breaker Tripped: Needs Human Review"
return End(ctx.state.final_result)
model_name = os.getenv("SUPERVISOR_AGENT_MODEL")
if not model_name:
raise ValueError("❌ [SSOT Violation] SUPERVISOR_AGENT_MODEL missing.")
from src.server.services.prompt_service import prompt_service
from src.server.utils import get_supabase_client
task_type = ctx.state.task_type
# 1. Try to load routing and prompt configuration from database dynamically
prompt_key = "WORKFLOW_SUPERVISOR_GENERAL"
node_routing = {
"marketbot": "MarketBotNode",
"librarian": "LibrarianNode",
"summary": "SummaryNode",
"devbot": "DevBotNode",
"david": "DavidNode"
}
try:
supabase = get_supabase_client()
res = supabase.table("archon_workflow_flows").select("*").eq("workflow_type", task_type).execute()
if res.data:
flow_data = res.data[0]
prompt_key = flow_data["supervisor_prompt_name"]
node_routing = flow_data["node_routing"]
except Exception:
pass
default_supervisor_prompt = (
"You are Charlie, the Supervisor. Review the conversation history. "
"Decide which worker should act next. "
"- 'marketbot' writes marketing content.\n"
"- 'librarian' searches documentation/RAG.\n"
"- 'summary' summarizes text.\n"
"- 'devbot' calculates statistics or writes code.\n"
"- 'david' extracts raw data from the database.\n"
"- 'end' if the goal is fully achieved.\n"
"- 'human' if you are stuck or lack permissions."
)
system_prompt = prompt_service.get_prompt(prompt_key, default_supervisor_prompt)
# Build agent config dynamically to avoid version mismatch errors
agent_args: dict[str, Any] = {"model": model_name, "system_prompt": system_prompt}
if PAI_V1:
agent_args["output_type"] = SupervisorDecision
else:
agent_args["result_type"] = SupervisorDecision
router_agent = Agent(**agent_args)
history_text = _build_pruned_history(ctx.state.messages)
try:
result = await _run_agent_with_retry(
router_agent, f"History:\n{history_text}\n\nDecide next step.", ctx.state, model_name
)
_accumulate_usage(ctx.state, result, model_name)
decision = _get_output(result)
logger.info(f"🧠 [Supervisor] Decision: {decision.next_node} (Reason: {decision.reasoning})")
next_step = decision.next_node
if next_step == "end":
ctx.state.final_result = "Workflow completed successfully."
return End(ctx.state.final_result)
elif next_step == "human":
ctx.state.final_result = "Escalated to human review."
return End(ctx.state.final_result)
# Map the next node routing name from database to static Class Node return signatures
target_node_name = node_routing.get(next_step)
if target_node_name == "MarketBotNode":
return MarketBotNode()
elif target_node_name == "LibrarianNode":
return LibrarianNode()
elif target_node_name == "SummaryNode":
return SummaryNode()
elif target_node_name == "DevBotNode":
return DevBotNode()
elif target_node_name == "DavidNode":
return DavidNode()
else:
ctx.state.final_result = f"Error: Unknown decision {next_step} or unmapped node {target_node_name}"
return End(ctx.state.final_result)
except Exception as e:
logger.error(f"Supervisor error: {e}", exc_info=True)
ctx.state.final_result = f"Supervisor Error: {str(e)}"
return End(ctx.state.final_result)
# --- 3. Worker Nodes (The Muscle) ---
async def _run_generic_worker(
ctx: GraphRunContext[SharedState],
role_name: str,
prompt_key: str,
default_prompt: str,
task_instruction: str,
) -> SupervisorNode:
logger.info(f"🛠️ [{role_name}] Executing task...")
model_name = os.getenv("WORKER_AGENT_MODEL")
if not model_name:
raise ValueError("❌ [SSOT Violation] WORKER_AGENT_MODEL missing.")
from src.server.services.prompt_service import prompt_service
system_prompt = prompt_service.get_prompt(prompt_key, default_prompt)
agent = Agent(model=model_name, system_prompt=system_prompt)
history_text = _build_pruned_history(ctx.state.messages)
try:
res = await _run_agent_with_retry(agent, f"{task_instruction}\n{history_text}", ctx.state, model_name)
_accumulate_usage(ctx.state, res, model_name)
ctx.state.messages.append({"role": role_name.lower(), "content": str(_get_output(res))})
except Exception as e:
logger.error(f"{role_name} error: {e}")
ctx.state.messages.append({"role": role_name.lower(), "content": f"Error: {e}"})
return SupervisorNode()
class MarketBotNode(BaseNode[SharedState, None, str]):
async def run(self, ctx: GraphRunContext[SharedState]) -> SupervisorNode:
task_type = ctx.state.task_type
prompt_key = (
"WORKFLOW_STRATEGIST_BOB" if task_type == "Marketing Data Deep Dive" else "WORKFLOW_WORKER_MARKETBOT"
)
return await _run_generic_worker(
ctx,
"MarketBot",
prompt_key,
"You are a marketing copywriter. Be concise. You MUST write your response in Traditional Chinese (繁體中文).",
"Based on history, provide the marketing copy.",
)
class LibrarianNode(BaseNode[SharedState, None, str]):
async def run(self, ctx: GraphRunContext[SharedState]) -> SupervisorNode:
logger.info("🛠️ [Librarian] Executing task...")
model_name = os.getenv("WORKER_AGENT_MODEL")
if not model_name:
raise ValueError("❌ [SSOT Violation] WORKER_AGENT_MODEL not found for LibrarianNode.")
from src.agents.rag_agent import RagAgent, RagDependencies
# Instantiate RagAgent which already has RAG tools registered
rag_agent_wrapper = RagAgent(model=model_name)
# Get the underlying PydanticAI agent to use with our retry helper
agent = rag_agent_wrapper._agent
# Setup dependencies for RAG tools
deps = RagDependencies(match_count=3)
history_text = _build_pruned_history(ctx.state.messages)
try:
# Phase 5.1.4: Hunter Mode - Librarian can now crawl external sites if internal search is insufficient
instruction = (
"Extract facts from history by searching available knowledge.\n"
"If the internal knowledge base does not contain the required information, "
"or if the user provides a specific URL, use the web_crawl_tool to get the latest data."
)
res = await _run_agent_with_retry(agent, f"{instruction}\n{history_text}", ctx.state, model_name, deps=deps)
_accumulate_usage(ctx.state, res, model_name)
# Phase 5.1.4: Citation Transparency - Pass collected citations to state
ctx.state.messages.append(
{"role": "librarian", "content": str(_get_output(res)), "citations": deps.collected_citations}
)
except Exception as e:
logger.error(f"Librarian error: {e}")
ctx.state.messages.append({"role": "librarian", "content": f"Error: {e}"})
return SupervisorNode()
class SummaryNode(BaseNode[SharedState, None, str]):
async def run(self, ctx: GraphRunContext[SharedState]) -> SupervisorNode:
return await _run_generic_worker(
ctx,
"Summary",
"WORKFLOW_WORKER_SUMMARY",
"You summarize text into bullet points. You MUST write your response in Traditional Chinese (繁體中文).",
"Summarize the conversation:",
)
class DevBotNode(BaseNode[SharedState, None, str]):
async def run(self, ctx: GraphRunContext[SharedState]) -> SupervisorNode:
await _run_generic_worker(
ctx, "DevBot", "WORKFLOW_SCIENTIST_DEVBOT", "You are DevBot, a data scientist. You MUST write your response in Traditional Chinese (繁體中文).", "Task from Supervisor:"
)
return SupervisorNode()
class DavidNode(BaseNode[SharedState, None, str]):
async def run(self, ctx: GraphRunContext[SharedState]) -> SupervisorNode:
logger.info("🛠️ [David] Thinking about code changes...")
model_name = os.getenv("WORKER_AGENT_MODEL")
if not model_name:
raise ValueError("❌ [SSOT Violation] WORKER_AGENT_MODEL missing.")
from src.server.services.prompt_service import prompt_service
system_prompt = prompt_service.get_prompt(
"WORKFLOW_DATA_DAVID",
"You are David, the Senior Developer. You can read code and propose fixes using tools. You MUST write your response in Traditional Chinese (繁體中文).",
)
agent = Agent(model=model_name, system_prompt=system_prompt, tools=[propose_code_fix, read_code_file])
history_text = _build_pruned_history(ctx.state.messages)
try:
res = await _run_agent_with_retry(
agent,
f"Review the history and use tools if needed to fix code or extract data.\n{history_text}",
ctx.state,
model_name,
)
_accumulate_usage(ctx.state, res, model_name)
ctx.state.messages.append({"role": "david", "content": str(_get_output(res))})
except Exception as e:
logger.error(f"David error: {e}")
ctx.state.messages.append({"role": "david", "content": f"Error: {e}"})
return SupervisorNode()