rak2315's picture
Deploy AgentSupport backend
ca76a2a
Raw
History Blame Contribute Delete
7.99 kB
import json
import os
import re
from typing import Any, Dict, List, Optional, TypedDict
from groq import Groq
from langgraph.graph import END, StateGraph
from agent.audit import log_step
from agent.tools import TOOL_MAP
from agent.validator import validate_tool_output
_groq = Groq(api_key=os.getenv("GROQ_API_KEY", ""))
_MODEL = "llama-3.3-70b-versatile"
MAX_STEPS = 8
class AgentState(TypedDict):
user_message: str
session_id: str
intent: str
pending_tools: List[Dict[str, Any]]
tool_results: List[Dict[str, Any]]
last_tool_name: Optional[str]
last_tool_input: Optional[Dict[str, Any]]
last_tool_output: Optional[Any]
step_count: int
final_response: str
def _groq_chat(system: str, user: str, temperature: float = 0.1) -> str:
resp = _groq.chat.completions.create(
model=_MODEL,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
temperature=temperature,
)
return resp.choices[0].message.content.strip()
def _extract_json(text: str) -> Dict:
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r"\{[\s\S]*\}", text)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
return {}
def intent_router(state: AgentState) -> Dict:
system = """You are a customer support AI. Analyze the customer message and plan the exact tool calls needed.
Available tools:
- order_lookup: Look up order details. Args: {"order_id": "ORD-XXX"}
- refund_trigger: Initiate a refund. Args: {"order_id": "ORD-XXX", "reason": "explanation"}
- ticket_create: Create a support ticket. Args: {"issue": "full description", "priority": "high|medium|low"}
- faq_retriever: Search policy/FAQ docs. Args: {"query": "search terms"}
Planning rules:
- Wrong item received → [order_lookup, refund_trigger, ticket_create] (high priority ticket)
- Damaged item → [order_lookup, refund_trigger, ticket_create] (high priority)
- Refund request → [order_lookup, refund_trigger]
- Order status/tracking → [order_lookup]
- Return policy / warranty / general questions → [faq_retriever]
- Explicit ticket request → [ticket_create]
- Extract order IDs exactly as written (e.g., ORD-003, ORD-001)
Respond ONLY with valid JSON:
{
"intent": "order_lookup|refund|ticket|faq|unknown",
"reasoning": "one sentence explaining the plan",
"tools": [
{"name": "tool_name", "args": {}}
]
}"""
raw = _groq_chat(system, state["user_message"])
parsed = _extract_json(raw)
intent = parsed.get("intent", "unknown")
tools = parsed.get("tools", [])
reasoning = parsed.get("reasoning", "Classified user intent and planned tool calls.")
log_step(
session_id=state["session_id"],
node_name="intent_router",
reasoning=reasoning,
tool_called=None,
tool_input={"user_message": state["user_message"]},
tool_output={"intent": intent, "planned_tools": [t.get("name") for t in tools]},
)
return {
"intent": intent,
"pending_tools": tools,
"tool_results": [],
"last_tool_name": None,
"last_tool_input": None,
"last_tool_output": None,
"step_count": state["step_count"] + 1,
}
def tool_executor(state: AgentState) -> Dict:
if not state["pending_tools"]:
return {"step_count": state["step_count"] + 1}
current = state["pending_tools"][0]
remaining = state["pending_tools"][1:]
tool_name = current.get("name", "")
tool_args = current.get("args", {})
tool_fn = TOOL_MAP.get(tool_name)
raw_result: Dict[str, Any] = {}
if tool_fn:
try:
raw = tool_fn.invoke(tool_args)
raw_result = raw if isinstance(raw, dict) else {"result": str(raw)}
except Exception as exc:
raw_result = {"error": str(exc), "tool": tool_name}
else:
raw_result = {"error": f"Unknown tool: {tool_name}"}
return {
"pending_tools": remaining,
"last_tool_name": tool_name,
"last_tool_input": tool_args,
"last_tool_output": raw_result,
"step_count": state["step_count"] + 1,
}
def action_validator(state: AgentState) -> Dict:
tool_name = state.get("last_tool_name") or ""
raw_output = state.get("last_tool_output") or {}
validated = validate_tool_output(tool_name, raw_output)
validated_dict = (
validated.model_dump() if hasattr(validated, "model_dump") else raw_output
)
updated_results = state["tool_results"] + [
{"tool": tool_name, "result": validated_dict}
]
return {
"last_tool_output": validated_dict,
"tool_results": updated_results,
"step_count": state["step_count"] + 1,
}
def audit_logger(state: AgentState) -> Dict:
tool_name = state.get("last_tool_name")
log_step(
session_id=state["session_id"],
node_name="audit_logger",
reasoning=f"Completed tool call: {tool_name}" if tool_name else "Step complete",
tool_called=tool_name,
tool_input=state.get("last_tool_input"),
tool_output=state.get("last_tool_output"),
)
return {"step_count": state["step_count"] + 1}
def responder(state: AgentState) -> Dict:
results_text = json.dumps(state["tool_results"], indent=2, default=str)
system = """You are a helpful customer support agent. Write a clear, friendly, concise response based on the tool results.
Be specific: mention order IDs, ticket IDs, refund timelines, and next steps.
Keep under 150 words. Do not mention internal tool names or system details."""
user = f"Customer message: {state['user_message']}\n\nTool results:\n{results_text}"
response = _groq_chat(system, user, temperature=0.3)
log_step(
session_id=state["session_id"],
node_name="responder",
reasoning="Generating final customer-facing response from all tool results",
tool_called=None,
tool_input={"num_tool_results": len(state["tool_results"])},
tool_output={"response_preview": response[:120] + "..." if len(response) > 120 else response},
)
return {"final_response": response, "step_count": state["step_count"] + 1}
def _route_after_audit(state: AgentState) -> str:
if state["step_count"] >= MAX_STEPS:
return "respond"
if state["pending_tools"]:
return "execute_tool"
return "respond"
def build_graph():
graph = StateGraph(AgentState)
graph.add_node("intent_router", intent_router)
graph.add_node("tool_executor", tool_executor)
graph.add_node("action_validator", action_validator)
graph.add_node("audit_logger", audit_logger)
graph.add_node("responder", responder)
graph.set_entry_point("intent_router")
graph.add_edge("intent_router", "tool_executor")
graph.add_edge("tool_executor", "action_validator")
graph.add_edge("action_validator", "audit_logger")
graph.add_conditional_edges(
"audit_logger",
_route_after_audit,
{"execute_tool": "tool_executor", "respond": "responder"},
)
graph.add_edge("responder", END)
return graph.compile()
_compiled_graph = None
def get_graph():
global _compiled_graph
if _compiled_graph is None:
_compiled_graph = build_graph()
return _compiled_graph
def run_agent(session_id: str, message: str) -> str:
graph = get_graph()
initial_state: AgentState = {
"user_message": message,
"session_id": session_id,
"intent": "",
"pending_tools": [],
"tool_results": [],
"last_tool_name": None,
"last_tool_input": None,
"last_tool_output": None,
"step_count": 0,
"final_response": "",
}
result = graph.invoke(initial_state)
return result.get("final_response", "I'm sorry, I wasn't able to process your request.")