Zainab4626's picture
Implement loop review pipeline, tool filtering, evals, and support-system A2A.
c89a66c
Raw
History Blame Contribute Delete
5.38 kB
"""Deterministic draft-review-refine pass for agent responses.
`RouterAgent` can call this after a domain agent returns a raw answer:
loop = LoopAgent()
result = loop.process(query=user_message, raw_answer=raw_answer, context={"routed_to": "billing"})
final_text = result["final_answer"]
This module is intentionally LLM-free so behavior is stable in local development and tests.
"""
from __future__ import annotations
import re
from typing import Any
class LoopAgent:
"""Runs a simple quality loop: draft -> review -> refine."""
def __init__(self, max_chars: int = 650) -> None:
"""Create a loop agent.
Args:
max_chars: Soft target for response length. Refinement trims answers above this limit.
"""
self._max_chars = max_chars
def process(self, query: str, raw_answer: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
"""Review and refine a raw answer before it is returned to the user.
Args:
query: The user's original question.
raw_answer: Initial answer produced by the routed domain agent.
context: Optional routing metadata (for example `{"routed_to": "billing"}`).
Returns:
dict with:
- `final_answer`: refined user-facing text
- `review_notes`: deterministic notes describing what was checked or changed
"""
_ = context or {}
draft = self._generate_draft(raw_answer)
review_notes = self._review_draft(query=query, draft=draft)
final_answer = self._refine_draft(draft=draft, review_notes=review_notes)
return {"final_answer": final_answer, "review_notes": review_notes}
def _generate_draft(self, raw_answer: str) -> str:
"""Generate the initial draft (currently passthrough)."""
return (raw_answer or "").strip()
def _review_draft(self, query: str, draft: str) -> list[str]:
"""Run deterministic quality checks for clarity, tone, and directness."""
notes: list[str] = []
lower = draft.lower()
if "todo" in lower:
notes.append("Contains TODO marker and needs cleanup.")
jargon_hits = [term for term in _INTERNAL_JARGON if term in lower]
if jargon_hits:
notes.append(f"Contains internal jargon: {', '.join(jargon_hits)}.")
if len(draft) > self._max_chars:
notes.append(f"Answer length ({len(draft)}) exceeds target ({self._max_chars}).")
# Clarity: flag very long single-paragraph responses.
if len(draft) > 220 and "\n" not in draft and ". " in draft:
notes.append("Could be clearer with shorter phrasing.")
# Tone: keep concise and friendly.
if not _looks_friendly(draft):
notes.append("Tone may feel abrupt; make it friendlier.")
# Directness: lightweight heuristic based on keyword overlap.
if not _directly_addresses_query(query=query, answer=draft):
notes.append("May not directly answer the user's question.")
if not notes:
notes.append("Review passed: clear, concise, and directly answers the question.")
return notes
def _refine_draft(self, draft: str, review_notes: list[str]) -> str:
"""Apply deterministic refinements based on review notes."""
refined = draft
# Remove placeholder TODOs.
refined = re.sub(r"\bTODO\b[:\- ]*", "", refined, flags=re.IGNORECASE)
# Replace known internal jargon with plain language.
for bad, plain in _JARGON_REPLACEMENTS.items():
refined = re.sub(rf"\b{re.escape(bad)}\b", plain, refined, flags=re.IGNORECASE)
# Add a soft-friendly opener only if the text is short and blunt.
if "Tone may feel abrupt; make it friendlier." in review_notes and refined:
if not _looks_friendly(refined):
refined = f"Happy to help. {refined}"
# Keep under a soft character budget where possible.
if len(refined) > self._max_chars:
cutoff = refined[: self._max_chars].rstrip()
last_sentence = cutoff.rfind(". ")
if last_sentence > 120:
refined = cutoff[: last_sentence + 1]
else:
refined = f"{cutoff}..."
# Final whitespace cleanup.
return re.sub(r"\s{2,}", " ", refined).strip()
_INTERNAL_JARGON = {
"mcp",
"json-rpc",
"remotea2aagent",
"llmagent",
"tool call",
"functiontool",
}
_JARGON_REPLACEMENTS = {
"mcp": "the service",
"json-rpc": "the integration channel",
"remotea2aagent": "the returns service",
"llmagent": "the assistant",
"functiontool": "tooling",
}
def _looks_friendly(text: str) -> bool:
"""Heuristic for friendly tone."""
lower = text.lower()
return any(token in lower for token in ("please", "happy to help", "thanks", "glad", "certainly"))
def _directly_addresses_query(query: str, answer: str) -> bool:
"""Lightweight check that answer overlaps the user query topic."""
query_words = {w for w in re.findall(r"[a-zA-Z]{4,}", query.lower())}
answer_words = {w for w in re.findall(r"[a-zA-Z]{4,}", answer.lower())}
if not query_words:
return bool(answer.strip())
overlap = query_words.intersection(answer_words)
return len(overlap) >= 1