refactor: remove scripted file intercept — LLM owns all responses

Previously ab_ai_mail.py intercepted file uploads before reaching the
LLM and responded with a hardcoded clarification template. The LLM had
no involvement in the file upload response.

Changes:
- ab_ai_mail.py: remove _post_file_clarification, _find_pending_attachments,
  _describe_zip, and the two-step pending-attachment lookup. All messages
  (text, files, or both) are dispatched to the agent service immediately.
  Files with no text pass an empty message — the LLM decides what to do.
- upload.py: default message changed from hardcoded receipt instruction
  to '' so the LLM determines intent from file content.
- master_agent._synthesize: always runs through the LLM for both single
  and multi-agent cases — no raw templates reach the user.
- master_system.txt: add FILE UPLOADS routing rule so the LLM knows to
  route receipts to expenses_agent without asking for clarification.

New flow: upload → parse → LLM classifies → agent acts → LLM synthesizes
natural response → user sees it. Zero scripted intercepts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Garcia
2026-05-19 21:05:38 -04:00
parent 0bd1810405
commit 93f2a101fa
4 changed files with 36 additions and 158 deletions

View File

@@ -285,22 +285,22 @@ class MasterAgent:
async def _synthesize(self, reports, context: MasterContext) -> str:
if not reports:
return 'No agent responses received.'
if len(reports) == 1:
return reports[0].summary or '(no summary)'
summaries = chr(10).join(f'{r.agent}: {r.summary}' for r in reports)
msg = ('Synthesize these agent reports into one coherent response. '
'Business language only. No internal IDs. '
'Separate: actions completed, items pending approval, recommendations.'
+ chr(10) + summaries)
summaries = chr(10).join(f'[{r.agent}]\n{r.summary}' for r in reports)
instruction = (
'You are ActiveBlue AI. Convert the following agent report(s) into a clear, '
'natural response for the user. Preserve every factual detail: amounts, dates, '
'record IDs, item names, and any action taken. Use plain text only — no JSON, '
'no markdown code fences. Write as if speaking directly to the user.'
)
try:
resp = await self._llm.submit(
[{'role': 'system', 'content': 'You are a business intelligence assistant.'},
{'role': 'user', 'content': msg}],
[{'role': 'system', 'content': instruction},
{'role': 'user', 'content': summaries}],
caller='master_synthesis')
return resp.content or summaries
return (resp.content or summaries).strip()
except Exception as exc:
logger.warning('_synthesize LLM call failed, falling back to raw summaries: %s', exc)
return summaries
logger.warning('_synthesize LLM call failed, using raw summary: %s', exc)
return reports[0].summary if len(reports) == 1 else summaries
async def _update_memory(self, user_id, message, response, reports, directive_id):
try:

View File

@@ -30,6 +30,10 @@ CRITICAL ROUTING RULE: Most messages are general conversation and require NO spe
Only route to a specialist agent when the user explicitly asks for Odoo data or actions.
When in doubt, use "agents": [].
FILE UPLOADS: When a user uploads files (message is empty or "User uploaded files"),
do NOT ask for clarification. Route directly to the appropriate agent based on file content.
Receipt images or PDFs → expenses_agent. Unknown files → agents: [] and answer directly.
Examples of correct routing:
User: "hello" or "hi" or "what can you do?" or "what does that mean?" or "ok" or "thanks"

View File

@@ -18,7 +18,7 @@ router = APIRouter(prefix='/upload', tags=['upload'])
async def upload(
request: Request,
user_id: str = Form(...),
message: str = Form(default='Create an employee expense report from these receipts.'),
message: str = Form(default=''),
session_id: Optional[str] = Form(default=None),
files: List[UploadFile] = File(default=[]),
):