system-prompts-and-models-o.../salesflow-saas/backend/app/services/agents/quality_gate.py

205 lines
6.9 KiB
Python

"""
Agent Quality Gate — Self-Correction Loop
==========================================
Runs the QA reviewer agent on other agents' outputs BEFORE they are dispatched.
This creates a two-pass system:
Pass 1: Agent generates output
Pass 2: QA agent validates → approve / reject / correct
Only approved outputs get dispatched to external services.
"""
import logging
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger("dealix.agents.quality_gate")
# Agents whose output should be QA'd before dispatch
QA_REQUIRED_AGENTS = {
"closer_agent",
"outreach_writer",
"proposal_drafter",
"arabic_whatsapp",
"english_conversation",
}
# Agents exempt from QA (meta-agents like QA itself, or low-risk)
QA_EXEMPT_AGENTS = {
"qa_reviewer",
"lead_qualification",
"knowledge_retrieval",
"revenue_attribution",
"management_summary",
"sector_strategist",
"ai_rehearsal",
}
# Minimum quality score to pass (out of 100)
MIN_QA_SCORE = 60
class QualityGate:
"""
Quality gate that intercepts agent outputs and validates them
before allowing dispatch to external services.
"""
def __init__(self, db: AsyncSession):
self.db = db
async def check(
self,
agent_type: str,
agent_output: dict,
input_data: dict,
tenant_id: str = None,
) -> dict:
"""
Run QA check on an agent's output.
Returns:
{
"approved": bool,
"qa_score": int,
"corrections": [...],
"violations": [...],
"corrected_output": dict | None,
}
"""
# Skip if agent is exempt
if agent_type in QA_EXEMPT_AGENTS:
return {"approved": True, "qa_score": 100, "reason": "exempt"}
# Skip if agent doesn't require QA
if agent_type not in QA_REQUIRED_AGENTS:
return {"approved": True, "qa_score": 100, "reason": "not_required"}
try:
from app.services.agents.executor import AgentExecutor
executor = AgentExecutor(self.db)
# Run QA reviewer on the output
qa_result = await executor.execute(
agent_type="qa_reviewer",
input_data={
"agent_type_reviewed": agent_type,
"conversation_content": str(agent_output.get("response_message_ar", ""))
or str(agent_output.get("draft_message", ""))
or str(agent_output),
"original_input": str(input_data)[:500],
},
tenant_id=tenant_id,
)
if qa_result.status != "success" or not qa_result.output:
logger.warning(f"QA reviewer failed for {agent_type}, auto-approving")
return {"approved": True, "qa_score": 75, "reason": "qa_error_passthrough"}
qa_output = qa_result.output
qa_score = qa_output.get("overall_score", 0)
violations = qa_output.get("violations", [])
improvements = qa_output.get("improvements", [])
# Check for critical violations
critical_violations = [
v for v in violations
if v.get("severity") == "high"
]
approved = (
qa_score >= MIN_QA_SCORE
and len(critical_violations) == 0
)
result = {
"approved": approved,
"qa_score": qa_score,
"qa_grade": qa_output.get("grade", ""),
"corrections": improvements,
"violations": violations,
"critical_violations": len(critical_violations),
"coaching_notes": qa_output.get("coaching_notes_ar", ""),
"corrected_output": None,
}
if not approved and qa_output.get("sample_better_response"):
result["corrected_output"] = {
"response_message_ar": qa_output["sample_better_response"],
}
logger.info(
f"QA Gate: agent={agent_type} score={qa_score} "
f"approved={approved} violations={len(violations)}"
)
return result
except Exception as e:
logger.error(f"Quality gate error for {agent_type}: {e}")
# On error, auto-approve to not block the pipeline
return {"approved": True, "qa_score": 50, "reason": f"gate_error: {e}"}
async def check_and_correct(
self,
agent_type: str,
agent_output: dict,
input_data: dict,
tenant_id: str = None,
max_retries: int = 1,
) -> tuple[dict, dict]:
"""
Check quality and auto-correct if needed.
Returns:
(final_output, qa_result)
"""
qa_result = await self.check(agent_type, agent_output, input_data, tenant_id)
if qa_result["approved"]:
return agent_output, qa_result
# If not approved but has corrected output, use it
if qa_result.get("corrected_output"):
logger.info(f"QA gate auto-corrected output for {agent_type}")
corrected = {**agent_output, **qa_result["corrected_output"]}
corrected["_qa_corrected"] = True
return corrected, qa_result
# If not approved and no correction, try re-running the agent with coaching
if max_retries > 0:
logger.info(f"QA gate requesting retry for {agent_type}")
coaching = qa_result.get("coaching_notes", "")
enhanced_input = {
**input_data,
"_qa_feedback": coaching,
"_qa_violations": str(qa_result.get("violations", [])),
"_retry_with_improvements": True,
}
try:
from app.services.agents.executor import AgentExecutor
executor = AgentExecutor(self.db)
retry_result = await executor.execute(
agent_type=agent_type,
input_data=enhanced_input,
tenant_id=tenant_id,
)
if retry_result.status == "success":
# Re-check the retried output (no more retries)
return await self.check_and_correct(
agent_type,
retry_result.output,
input_data,
tenant_id,
max_retries=0,
)
except Exception as e:
logger.warning(f"QA retry failed for {agent_type}: {e}")
# Final fallback: return original with warning
agent_output["_qa_warning"] = "Output below quality threshold"
agent_output["_qa_score"] = qa_result.get("qa_score", 0)
return agent_output, qa_result