- 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>
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
import json
|
|
import logging
|
|
import requests
|
|
|
|
from odoo import http
|
|
from odoo.http import request, Response
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AiApprovalController(http.Controller):
|
|
|
|
@http.route('/ai/approval/pending', type='json', auth='user', methods=['GET'])
|
|
def list_pending(self):
|
|
if not request.env.user.has_group('activeblue_ai.group_ai_manager'):
|
|
return {'error': 'Access denied', 'items': []}
|
|
bot = request.env['ab.ai.bot'].sudo().search([('active', '=', True)], limit=1)
|
|
if not bot:
|
|
return {'items': []}
|
|
url = bot._get_service_url() + '/approval/pending'
|
|
try:
|
|
resp = requests.get(url, headers=bot._build_headers(), timeout=10)
|
|
resp.raise_for_status()
|
|
return {'items': resp.json()}
|
|
except Exception as exc:
|
|
_logger.error('list_pending failed: %s', exc)
|
|
return {'error': str(exc), 'items': []}
|
|
|
|
@http.route('/ai/approval/respond', type='json', auth='user', methods=['POST'])
|
|
def respond(self, directive_id, approved, note=None):
|
|
if not request.env.user.has_group('activeblue_ai.group_ai_manager'):
|
|
return {'error': 'Access denied'}
|
|
bot = request.env['ab.ai.bot'].sudo().search([('active', '=', True)], limit=1)
|
|
if not bot:
|
|
return {'error': 'No bot configured'}
|
|
url = bot._get_service_url() + '/approval/respond'
|
|
payload = {
|
|
'directive_id': directive_id,
|
|
'approved': approved,
|
|
'approver_id': str(request.env.user.id),
|
|
'note': note or '',
|
|
}
|
|
try:
|
|
resp = requests.post(url, json=payload, headers=bot._build_headers(), timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception as exc:
|
|
_logger.error('respond approval failed: %s', exc)
|
|
return {'error': str(exc)}
|
|
|
|
@http.route('/ai/chat', type='json', auth='user', methods=['POST'])
|
|
def chat(self, message, context=None, session_id=None):
|
|
bot = request.env['ab.ai.bot'].sudo().get_active_bot()
|
|
result = bot.dispatch_message(
|
|
user_id=request.env.user.id,
|
|
message=message,
|
|
context=context or {},
|
|
session_id=session_id,
|
|
)
|
|
return result
|
|
|
|
@http.route('/ai/upload', type='http', auth='user', methods=['POST'], csrf=False)
|
|
def upload(self, **kwargs):
|
|
bot = request.env['ab.ai.bot'].sudo().get_active_bot()
|
|
if not bot:
|
|
return Response(
|
|
json.dumps({'error': 'No bot configured'}),
|
|
content_type='application/json', status=503)
|
|
|
|
url = bot._get_service_url() + '/upload'
|
|
message = request.httprequest.form.get(
|
|
'message', 'Create an employee expense report from these receipts.')
|
|
session_id = request.httprequest.form.get('session_id', '')
|
|
|
|
files_data = [
|
|
('files', (f.filename, f.read(), f.content_type or 'application/octet-stream'))
|
|
for f in request.httprequest.files.getlist('files')
|
|
]
|
|
|
|
try:
|
|
resp = requests.post(
|
|
url,
|
|
data={
|
|
'user_id': str(request.env.user.id),
|
|
'message': message,
|
|
'session_id': session_id,
|
|
},
|
|
files=files_data or [('files', ('empty', b'', 'application/octet-stream'))],
|
|
headers=bot._build_headers(),
|
|
timeout=120,
|
|
)
|
|
resp.raise_for_status()
|
|
return Response(resp.text, content_type='application/json')
|
|
except Exception as exc:
|
|
_logger.error('upload proxy failed: %s', exc)
|
|
return Response(
|
|
json.dumps({'error': str(exc), 'reply': 'Upload failed. Please try again.'}),
|
|
content_type='application/json', status=500)
|