feat: file upload + expense report creation from Discuss attachments

- Discuss bot now reads ir.attachment from incoming messages; file-only
  messages no longer silently dropped
- ZIP files are described (contents listed) and bot asks clarifying
  question before acting; user's follow-up reply looks back for pending
  attachments so files don't need to be re-uploaded
- receipt_parser: extracts text from ZIP (recursive), JPG/PNG/etc (OCR),
  PDF (pdfplumber), HTML, TXT
- expenses_agent: full rewrite fixing broken method signatures; adds
  create_expense_sheet / create_expense / attach_receipt flow driven by
  LLM receipt parsing (Ollama, HIPAA-locked)
- master_agent: extra_context threads receipts + user_id into directives
- FastAPI /upload multipart endpoint; registered in main.py
- Odoo /ai/upload controller proxies files to agent service
- ab_ai_bot: dispatch_message_with_files() for multipart uploads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Garcia
2026-05-16 01:02:24 -04:00
parent bee8e20580
commit 4b7223a139
11 changed files with 658 additions and 45 deletions

View File

@@ -71,7 +71,8 @@ class MasterAgent:
# example block, so str.format would treat them as fields.
return template.replace('{agent_list}', agent_list)
async def handle_message(self, user_id, channel_id, message, directive_id) -> MasterResponse:
async def handle_message(self, user_id, channel_id, message, directive_id,
extra_context: dict = None) -> MasterResponse:
try:
user_id = int(user_id)
except (TypeError, ValueError):
@@ -107,7 +108,8 @@ class MasterAgent:
await self._memory.append_message(user_id, 'assistant', msg, directive_id)
await self._log_directive_complete(directive_id, 'failed', msg)
return MasterResponse(directive_id=directive_id, response=msg, status='failed')
directives = await self._build_directives(intent, context, directive_id)
directives = await self._build_directives(intent, context, directive_id,
user_id=user_id, extra_context=extra_context)
reports = await self._dispatch_agents(directives)
response_text = await self._synthesize(reports, context)
await self._update_memory(user_id, message, response_text, reports, directive_id)
@@ -197,20 +199,26 @@ class MasterAgent:
return AccessResult(allowed=False, denied_agents=denied)
return AccessResult(allowed=True)
async def _build_directives(self, intent: IntentResult, context: MasterContext, directive_id) -> list:
async def _build_directives(self, intent: IntentResult, context: MasterContext, directive_id,
user_id=None, extra_context: dict = None) -> list:
receipts = (extra_context or {}).get('receipts', [])
directives = []
for agent_key in intent.agents:
authorized = ['read', 'search', 'report', 'post_chatter',
'send_email', 'create_non_financial', 'write_non_financial']
if receipts:
authorized.append('create_expense')
ctx = DirectiveContext(
client_profile=context.knowledge,
recent_findings=context.operational_findings,
conversation_summary=chr(10).join(
m['content'] for m in context.conversation[-5:] if m['role'] == 'assistant'),
peer_data={})
peer_data={'requesting_user_id': user_id},
receipts=receipts)
d = AgentDirective(
directive_id=directive_id, agent=agent_key, task=intent.intent_summary,
params=intent.params, context=ctx,
authorized_actions=['read', 'search', 'report', 'post_chatter',
'send_email', 'create_non_financial', 'write_non_financial'],
authorized_actions=authorized,
constraints={'max_amount': 5000})
directives.append(d)
return directives