mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 23:39:34 +00:00
348 lines
13 KiB
Python
348 lines
13 KiB
Python
"""
|
|
Agent Executor — Runs AI agents with LLM calls, input validation,
|
|
output parsing, escalation checks, and action dispatch.
|
|
|
|
This is the engine that powers every single AI agent in Dealix.
|
|
"""
|
|
|
|
import time
|
|
import uuid
|
|
import json
|
|
import logging
|
|
from typing import Optional
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.services.llm.provider import get_llm, LLMResponse
|
|
from app.services.agents.router import AgentRouter
|
|
|
|
logger = logging.getLogger("dealix.agents.executor")
|
|
|
|
# Load prompt files path
|
|
PROMPTS_DIR = Path(__file__).parent.parent.parent.parent.parent / "ai-agents" / "prompts"
|
|
|
|
|
|
class AgentResult:
|
|
"""Standardized agent execution result."""
|
|
def __init__(self, agent_type: str, output: dict, tokens_used: int = 0,
|
|
latency_ms: int = 0, status: str = "success",
|
|
escalation: dict = None, actions: list = None):
|
|
self.agent_type = agent_type
|
|
self.output = output
|
|
self.tokens_used = tokens_used
|
|
self.latency_ms = latency_ms
|
|
self.status = status # success, error, escalated
|
|
self.escalation = escalation # {needed: bool, reason: str, target: str}
|
|
self.actions = actions or [] # [{type: "send_message", ...}, ...]
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"agent_type": self.agent_type,
|
|
"output": self.output,
|
|
"tokens_used": self.tokens_used,
|
|
"latency_ms": self.latency_ms,
|
|
"status": self.status,
|
|
"escalation": self.escalation,
|
|
"actions": self.actions,
|
|
}
|
|
|
|
|
|
class AgentExecutor:
|
|
"""
|
|
Executes AI agents by:
|
|
1. Loading the agent's system prompt
|
|
2. Building context from input data
|
|
3. Calling the LLM
|
|
4. Parsing structured output
|
|
5. Checking escalation rules
|
|
6. Dispatching actions (DB updates, messages, bookings)
|
|
7. Logging to ai_conversations
|
|
"""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
self.llm = get_llm()
|
|
self.router = AgentRouter()
|
|
|
|
async def execute(self, agent_type: str, input_data: dict,
|
|
tenant_id: str = None, lead_id: str = None,
|
|
conversation_id: str = None) -> AgentResult:
|
|
"""Execute an AI agent and return structured result."""
|
|
start = time.time()
|
|
|
|
try:
|
|
# 1. Load system prompt
|
|
system_prompt = self._load_prompt(agent_type)
|
|
|
|
# 2. Build user message from input data
|
|
user_message = self._build_user_message(agent_type, input_data)
|
|
|
|
# 3. Call LLM
|
|
llm_response = await self.llm.complete(
|
|
system_prompt=system_prompt,
|
|
user_message=user_message,
|
|
json_mode=True,
|
|
temperature=self._get_temperature(agent_type),
|
|
max_tokens=self._get_max_tokens(agent_type),
|
|
)
|
|
|
|
# 4. Parse output
|
|
output = llm_response.parse_json()
|
|
if output is None:
|
|
output = {"raw_response": llm_response.content}
|
|
|
|
# 5. Check escalation
|
|
escalation = self._check_escalation(agent_type, output, input_data)
|
|
|
|
# 6. Build actions
|
|
actions = self._build_actions(agent_type, output, input_data)
|
|
|
|
latency = int((time.time() - start) * 1000)
|
|
|
|
result = AgentResult(
|
|
agent_type=agent_type,
|
|
output=output,
|
|
tokens_used=llm_response.tokens_used,
|
|
latency_ms=latency,
|
|
status="escalated" if escalation and escalation.get("needed") else "success",
|
|
escalation=escalation,
|
|
actions=actions,
|
|
)
|
|
|
|
# 7. Log to database
|
|
await self._log_conversation(
|
|
tenant_id=tenant_id,
|
|
agent_type=agent_type,
|
|
lead_id=lead_id,
|
|
input_data=input_data,
|
|
output=result.to_dict(),
|
|
tokens_used=llm_response.tokens_used,
|
|
latency_ms=latency,
|
|
status=result.status,
|
|
)
|
|
|
|
logger.info(
|
|
f"Agent {agent_type} executed: "
|
|
f"tokens={llm_response.tokens_used} "
|
|
f"latency={latency}ms "
|
|
f"status={result.status}"
|
|
)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
latency = int((time.time() - start) * 1000)
|
|
logger.error(f"Agent {agent_type} failed: {e}")
|
|
|
|
result = AgentResult(
|
|
agent_type=agent_type,
|
|
output={"error": str(e)},
|
|
latency_ms=latency,
|
|
status="error",
|
|
)
|
|
|
|
await self._log_conversation(
|
|
tenant_id=tenant_id,
|
|
agent_type=agent_type,
|
|
lead_id=lead_id,
|
|
input_data=input_data,
|
|
output=result.to_dict(),
|
|
tokens_used=0,
|
|
latency_ms=latency,
|
|
status="error",
|
|
)
|
|
|
|
return result
|
|
|
|
async def execute_event(self, event_type: str, input_data: dict,
|
|
tenant_id: str = None, **kwargs) -> list[AgentResult]:
|
|
"""Execute all agents registered for an event type."""
|
|
agent_ids = self.router.get_agents_for_event(event_type)
|
|
results = []
|
|
|
|
for agent_id in agent_ids:
|
|
result = await self.execute(
|
|
agent_type=agent_id,
|
|
input_data=input_data,
|
|
tenant_id=tenant_id,
|
|
**kwargs,
|
|
)
|
|
results.append(result)
|
|
|
|
# Stop chain if escalation needed
|
|
if result.escalation and result.escalation.get("needed"):
|
|
logger.info(f"Agent chain stopped at {agent_id} due to escalation")
|
|
break
|
|
|
|
return results
|
|
|
|
# ── Prompt Loading ──────────────────────────────
|
|
|
|
def _load_prompt(self, agent_type: str) -> str:
|
|
"""Load system prompt from the ai-agents/prompts directory."""
|
|
# Map agent_type to filename
|
|
filename_map = {
|
|
"closer_agent": "closer-agent.md",
|
|
"lead_qualification": "lead-qualification-agent.md",
|
|
"arabic_whatsapp": "arabic-whatsapp-agent.md",
|
|
"english_conversation": "english-conversation-agent.md",
|
|
"outreach_writer": "outreach-message-writer.md",
|
|
"meeting_booking": "meeting-booking-agent.md",
|
|
"objection_handler": "objection-handling-agent.md",
|
|
"proposal_drafter": "proposal-drafting-agent.md",
|
|
"sector_strategist": "sector-sales-strategist.md",
|
|
"knowledge_retrieval": "knowledge-retrieval-agent.md",
|
|
"compliance_reviewer": "compliance-reviewer.md",
|
|
"fraud_reviewer": "fraud-reviewer.md",
|
|
"revenue_attribution": "revenue-attribution-agent.md",
|
|
"management_summary": "management-summary-agent.md",
|
|
"qa_reviewer": "conversation-qa-reviewer.md",
|
|
"affiliate_evaluator": "affiliate-recruitment-evaluator.md",
|
|
"onboarding_coach": "affiliate-onboarding-coach.md",
|
|
"guarantee_reviewer": "guarantee-claim-reviewer.md",
|
|
"voice_call": "voice-call-flow-agent.md",
|
|
}
|
|
|
|
filename = filename_map.get(agent_type)
|
|
if not filename:
|
|
return f"You are the {agent_type} agent for Dealix. Respond with structured JSON."
|
|
|
|
# Check primary prompts dir
|
|
prompt_path = PROMPTS_DIR / filename
|
|
if prompt_path.exists():
|
|
return prompt_path.read_text(encoding="utf-8")
|
|
|
|
# Check fallback backend prompts dir
|
|
backend_prompts_dir = Path(__file__).parent.parent.parent / "ai" / "prompts"
|
|
fallback_path = backend_prompts_dir / filename
|
|
if fallback_path.exists():
|
|
return fallback_path.read_text(encoding="utf-8")
|
|
|
|
logger.warning(f"Prompt file not found for {agent_type}: {filename}")
|
|
return f"You are the {agent_type} agent for Dealix. Respond with structured JSON."
|
|
|
|
def _build_user_message(self, agent_type: str, input_data: dict) -> str:
|
|
"""Build the user message from input data."""
|
|
# General format: JSON dump of input data with clear instructions
|
|
context = json.dumps(input_data, ensure_ascii=False, indent=2, default=str)
|
|
|
|
return f"""## Input Data
|
|
{context}
|
|
|
|
## Instructions
|
|
Process this input according to your role and return a structured JSON response.
|
|
Include all required output fields as defined in your schema.
|
|
Use Arabic where appropriate (especially for client-facing content).
|
|
Respond ONLY with valid JSON."""
|
|
|
|
# ── Configuration per Agent ────────────────────
|
|
|
|
def _get_temperature(self, agent_type: str) -> float:
|
|
"""Agent-specific temperature settings."""
|
|
# Creative agents need higher temperature
|
|
creative = {"outreach_writer": 0.7, "proposal_drafter": 0.5, "sector_strategist": 0.5}
|
|
# Analytical agents need low temperature
|
|
analytical = {
|
|
"lead_qualification": 0.1, "compliance_reviewer": 0.1,
|
|
"fraud_reviewer": 0.1, "revenue_attribution": 0.1,
|
|
}
|
|
return creative.get(agent_type, analytical.get(agent_type, 0.3))
|
|
|
|
def _get_max_tokens(self, agent_type: str) -> int:
|
|
"""Agent-specific max token settings."""
|
|
verbose = {"proposal_drafter": 4096, "management_summary": 4096, "sector_strategist": 3000}
|
|
return verbose.get(agent_type, 2048)
|
|
|
|
# ── Escalation Rules ──────────────────────────
|
|
|
|
def _check_escalation(self, agent_type: str, output: dict, input_data: dict) -> Optional[dict]:
|
|
"""Check if the agent output requires escalation to a human."""
|
|
escalation = output.get("escalation", {})
|
|
if isinstance(escalation, dict) and escalation.get("needed"):
|
|
return escalation
|
|
|
|
# Agent-specific checks
|
|
if agent_type == "arabic_whatsapp":
|
|
confidence = output.get("confidence", 1.0)
|
|
if confidence < 0.5:
|
|
return {"needed": True, "reason": "Low confidence response", "target": "human_agent"}
|
|
|
|
if agent_type == "lead_qualification":
|
|
score = output.get("score", 50)
|
|
if 40 <= score <= 60:
|
|
return {"needed": True, "reason": "Ambiguous qualification score", "target": "sales_manager"}
|
|
|
|
if agent_type == "fraud_reviewer":
|
|
risk_score = output.get("risk_score", 0)
|
|
if risk_score > 80:
|
|
return {"needed": True, "reason": "High fraud risk detected", "target": "admin"}
|
|
|
|
return None
|
|
|
|
# ── Action Building ───────────────────────────
|
|
|
|
def _build_actions(self, agent_type: str, output: dict, input_data: dict) -> list:
|
|
"""Build a list of actions to execute based on agent output."""
|
|
actions = []
|
|
|
|
if agent_type == "arabic_whatsapp" and output.get("response_message_ar"):
|
|
actions.append({
|
|
"type": "send_whatsapp",
|
|
"message": output["response_message_ar"],
|
|
"phone": input_data.get("contact_phone", ""),
|
|
})
|
|
|
|
if agent_type == "meeting_booking" and output.get("meeting_booked", {}).get("confirmed"):
|
|
actions.append({
|
|
"type": "create_meeting",
|
|
"datetime": output["meeting_booked"].get("datetime"),
|
|
"lead_id": input_data.get("lead_id"),
|
|
})
|
|
|
|
if agent_type == "outreach_writer" and output.get("draft_message"):
|
|
actions.append({
|
|
"type": "queue_message",
|
|
"channel": input_data.get("channel", "whatsapp"),
|
|
"message": output["draft_message"],
|
|
})
|
|
|
|
if agent_type == "lead_qualification":
|
|
actions.append({
|
|
"type": "update_lead_score",
|
|
"lead_id": input_data.get("lead_id"),
|
|
"score": output.get("score", 0),
|
|
"status": output.get("status_recommendation", "contacted"),
|
|
})
|
|
|
|
return actions
|
|
|
|
# ── Database Logging ──────────────────────────
|
|
|
|
async def _log_conversation(self, tenant_id: str, agent_type: str,
|
|
lead_id: str, input_data: dict, output: dict,
|
|
tokens_used: int, latency_ms: int, status: str):
|
|
"""Log agent execution to ai_conversations table."""
|
|
try:
|
|
from app.models.ai_conversation import AIConversation
|
|
|
|
log_entry = AIConversation(
|
|
tenant_id=uuid.UUID(tenant_id) if tenant_id else None,
|
|
contact_name=input_data.get("contact_name"),
|
|
contact_phone=input_data.get("contact_phone"),
|
|
channel="system",
|
|
status=status,
|
|
lead_id=uuid.UUID(lead_id) if lead_id else None,
|
|
context={
|
|
"agent_type": agent_type,
|
|
"input": input_data,
|
|
"output": output,
|
|
"tokens_used": tokens_used,
|
|
"latency_ms": latency_ms,
|
|
},
|
|
)
|
|
self.db.add(log_entry)
|
|
await self.db.flush()
|
|
except Exception as e:
|
|
logger.error(f"Failed to log agent conversation: {e}")
|