fix: prevent master agent asking for clarification when receipts are uploaded
When a zip/image arrives via /upload, the LLM was classifying the message as needs_clarification=True (because the chat body was just a filename like "download (8).zip", not an instruction), and the early return on line 91 fired before the receipts safety guard on line 106, so the guard never executed. master_agent: move the receipts safety guard to BEFORE the needs_clarification early-return. If extra_context contains receipts, unconditionally set needs_clarification=False and ensure expenses_agent is in the agents list — the LLM cannot veto an upload with a question. upload router: normalize empty or filename-only messages (e.g. when the user drops a file in Discuss chat with no text) to "Create an expense report from these uploaded receipts." so the LLM intent classification also has a sensible string to work with. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,17 @@ class MasterAgent:
|
|||||||
await self._memory.append_message(user_id, 'user', message, directive_id)
|
await self._memory.append_message(user_id, 'user', message, directive_id)
|
||||||
context = await self._build_context(user_id, message)
|
context = await self._build_context(user_id, message)
|
||||||
intent = await self._classify_intent(context, message)
|
intent = await self._classify_intent(context, message)
|
||||||
|
|
||||||
|
# Safety guard: when receipts arrive via the upload flow, always
|
||||||
|
# route to expenses_agent — regardless of what the LLM decided.
|
||||||
|
# MUST run BEFORE the needs_clarification early-return; otherwise an
|
||||||
|
# LLM that asks "please provide more context" blocks a valid upload.
|
||||||
|
if (extra_context or {}).get('receipts'):
|
||||||
|
intent.needs_clarification = False
|
||||||
|
intent.clarification_question = None
|
||||||
|
if 'expenses_agent' not in intent.agents:
|
||||||
|
intent.agents.append('expenses_agent')
|
||||||
|
|
||||||
if intent.needs_clarification:
|
if intent.needs_clarification:
|
||||||
q = intent.clarification_question or 'Could you provide more details?'
|
q = intent.clarification_question or 'Could you provide more details?'
|
||||||
await self._memory.append_message(user_id, 'assistant', q, directive_id)
|
await self._memory.append_message(user_id, 'assistant', q, directive_id)
|
||||||
@@ -101,11 +112,6 @@ class MasterAgent:
|
|||||||
await self._log_directive_complete(directive_id, 'complete', response_text)
|
await self._log_directive_complete(directive_id, 'complete', response_text)
|
||||||
return MasterResponse(directive_id=directive_id, response=response_text,
|
return MasterResponse(directive_id=directive_id, response=response_text,
|
||||||
status='complete')
|
status='complete')
|
||||||
# When receipts are present (upload flow), always dispatch expenses_agent
|
|
||||||
# even if the user's message is a one-word reply like "skip duplicates".
|
|
||||||
if (extra_context or {}).get('receipts') and 'expenses_agent' not in intent.agents:
|
|
||||||
intent.agents.append('expenses_agent')
|
|
||||||
intent.needs_clarification = False
|
|
||||||
access = await self._check_access(user_id, intent.agents)
|
access = await self._check_access(user_id, intent.agents)
|
||||||
if not access.allowed:
|
if not access.allowed:
|
||||||
denied = ', '.join(access.denied_agents)
|
denied = ', '.join(access.denied_agents)
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
# Matches messages that are just a filename (e.g. "download (8).zip", "receipt.jpg").
|
||||||
|
# When the chat body is only the filename, the LLM has nothing useful to classify.
|
||||||
|
_FILENAME_ONLY_RE = re.compile(r'^[\w\s\-.()\[\]]+\.\w{2,6}$')
|
||||||
|
|
||||||
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile, status
|
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile, status
|
||||||
|
|
||||||
from ..config import get_settings
|
from ..config import get_settings
|
||||||
@@ -35,6 +40,14 @@ async def upload(
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
_ocr_executor = ThreadPoolExecutor(max_workers=2)
|
_ocr_executor = ThreadPoolExecutor(max_workers=2)
|
||||||
|
|
||||||
|
# Normalise message: if the user sent no text (or the chat body is just the
|
||||||
|
# filename Odoo auto-inserts), give the master agent a clear instruction so
|
||||||
|
# it routes to expenses_agent rather than asking for clarification.
|
||||||
|
stripped = (message or '').strip()
|
||||||
|
if not stripped or _FILENAME_ONLY_RE.match(stripped):
|
||||||
|
message = 'Create an expense report from these uploaded receipts.'
|
||||||
|
logger.debug('upload: normalised empty/filename message for user_id=%s', user_id)
|
||||||
|
|
||||||
receipts: list[dict] = []
|
receipts: list[dict] = []
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
for f in files:
|
for f in files:
|
||||||
|
|||||||
Reference in New Issue
Block a user