fix: align peer_bus signature, bot presence SQL, XML-RPC timeout
- All specialist agents: handle_peer_request(request_type, params, directive_id) replaces handle_peer_request(request: dict) so callers pass structured args - ab_ai_bot: force-write bus_presence.status via SQL so Odoo 18 WebSocket presence shows the correct colour immediately (ORM compute does not trigger on last_poll writes) - odoo_client: wrap XML-RPC executor calls in asyncio.wait_for to enforce timeout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -168,22 +168,33 @@ class AbAiBot(models.Model):
|
|||||||
try:
|
try:
|
||||||
Presence = self.env['bus.presence']
|
Presence = self.env['bus.presence']
|
||||||
now = fields.Datetime.now()
|
now = fields.Datetime.now()
|
||||||
# bus.presence.status is a computed field — write only last_poll/last_presence.
|
status = 'online' if online else 'offline'
|
||||||
# When online: set both 24h ahead so the bot stays "online" regardless of
|
|
||||||
# cron timing. The cron explicitly marks offline by setting them to the past.
|
|
||||||
if online:
|
if online:
|
||||||
poll_time = now + timedelta(minutes=10)
|
poll_time = now + timedelta(minutes=10)
|
||||||
presence_time = now + timedelta(minutes=10)
|
presence_time = now + timedelta(minutes=10)
|
||||||
else:
|
else:
|
||||||
poll_time = now - timedelta(hours=1)
|
poll_time = now - timedelta(hours=1)
|
||||||
presence_time = now - timedelta(hours=1)
|
presence_time = now - timedelta(hours=1)
|
||||||
vals = {'last_poll': poll_time, 'last_presence': presence_time}
|
|
||||||
rec = Presence.sudo().search([('user_id', '=', bot_user.id)], limit=1)
|
rec = Presence.sudo().search([('user_id', '=', bot_user.id)], limit=1)
|
||||||
if rec:
|
if rec:
|
||||||
rec.write(vals)
|
rec.write({'last_poll': poll_time, 'last_presence': presence_time})
|
||||||
|
# Force-update stored status column directly — Odoo 18 WebSocket-based
|
||||||
|
# presence doesn't trigger the stored compute when last_poll is written via ORM.
|
||||||
|
self.env.cr.execute(
|
||||||
|
"UPDATE bus_presence SET status = %s WHERE user_id = %s",
|
||||||
|
(status, bot_user.id),
|
||||||
|
)
|
||||||
|
rec.invalidate_recordset(['status'])
|
||||||
else:
|
else:
|
||||||
vals['user_id'] = bot_user.id
|
Presence.sudo().create({
|
||||||
Presence.sudo().create(vals)
|
'user_id': bot_user.id,
|
||||||
|
'last_poll': poll_time,
|
||||||
|
'last_presence': presence_time,
|
||||||
|
})
|
||||||
|
self.env.cr.execute(
|
||||||
|
"UPDATE bus_presence SET status = %s WHERE user_id = %s",
|
||||||
|
(status, bot_user.id),
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_logger.warning('Could not update bot user presence: %s', exc)
|
_logger.warning('Could not update bot user presence: %s', exc)
|
||||||
|
|
||||||
|
|||||||
@@ -120,16 +120,15 @@ class AccountingAgent(BaseAgent):
|
|||||||
return await self._at.post_chatter_note(**args)
|
return await self._at.post_chatter_note(**args)
|
||||||
raise ValueError(f'Unknown tool: {name}')
|
raise ValueError(f'Unknown tool: {name}')
|
||||||
|
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'trial_balance':
|
if request_type == 'trial_balance':
|
||||||
return {'trial_balance': await self._at.get_trial_balance()}
|
return {'trial_balance': await self._at.get_trial_balance()}
|
||||||
if req_type == 'account_balance':
|
if request_type == 'account_balance':
|
||||||
return await self._at.get_account_balance(account_id=request['account_id'])
|
return await self._at.get_account_balance(account_id=params['account_id'])
|
||||||
if req_type == 'tax_summary':
|
if request_type == 'tax_summary':
|
||||||
return await self._at.get_tax_summary()
|
return await self._at.get_tax_summary()
|
||||||
return {'error': f'Unknown type: {req_type}'}
|
return {'error': f'Unknown type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -115,14 +115,13 @@ class CrmAgent(BaseAgent):
|
|||||||
raise ValueError(f'Unknown tool: {name}')
|
raise ValueError(f'Unknown tool: {name}')
|
||||||
return await dispatch[name](**args)
|
return await dispatch[name](**args)
|
||||||
|
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'pipeline_summary':
|
if request_type == 'pipeline_summary':
|
||||||
return await self._ct.get_pipeline_summary()
|
return await self._ct.get_pipeline_summary()
|
||||||
if req_type == 'opportunities':
|
if request_type == 'opportunities':
|
||||||
return {'opportunities': await self._ct.get_opportunities(user_id=request.get('user_id'))}
|
return {'opportunities': await self._ct.get_opportunities(user_id=params.get('user_id'))}
|
||||||
return {'error': f'Unknown type: {req_type}'}
|
return {'error': f'Unknown type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -127,17 +127,16 @@ class EmployeesAgent(BaseAgent):
|
|||||||
raise ValueError(f'Unknown tool: {name}')
|
raise ValueError(f'Unknown tool: {name}')
|
||||||
return await dispatch[name](**args)
|
return await dispatch[name](**args)
|
||||||
|
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'employee_list':
|
if request_type == 'employee_list':
|
||||||
return {'employees': await self._ht.get_employees(department_id=request.get('department_id'))}
|
return {'employees': await self._ht.get_employees(department_id=params.get('department_id'))}
|
||||||
if req_type == 'employee_profile':
|
if request_type == 'employee_profile':
|
||||||
return await self._ht.get_employee_profile(employee_id=request['employee_id'])
|
return await self._ht.get_employee_profile(employee_id=params['employee_id'])
|
||||||
if req_type == 'headcount':
|
if request_type == 'headcount':
|
||||||
employees = await self._ht.get_employees(department_id=request.get('department_id'))
|
employees = await self._ht.get_employees(department_id=params.get('department_id'))
|
||||||
return {'headcount': len(employees)}
|
return {'headcount': len(employees)}
|
||||||
return {'error': f'Unknown type: {req_type}'}
|
return {'error': f'Unknown type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -430,15 +430,14 @@ class ExpensesAgent(BaseAgent):
|
|||||||
raise ValueError(f'Unknown tool: {name}')
|
raise ValueError(f'Unknown tool: {name}')
|
||||||
return await dispatch[name](**args)
|
return await dispatch[name](**args)
|
||||||
|
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'expenses_summary':
|
if request_type == 'expenses_summary':
|
||||||
return await self._et.get_expenses_summary()
|
return await self._et.get_expenses_summary()
|
||||||
if req_type == 'employee_expenses':
|
if request_type == 'employee_expenses':
|
||||||
return {'expenses': await self._et.get_expense_by_employee(
|
return {'expenses': await self._et.get_expense_by_employee(
|
||||||
employee_id=request['employee_id'])}
|
employee_id=params['employee_id'])}
|
||||||
return {'error': f'Unknown type: {req_type}'}
|
return {'error': f'Unknown type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -283,30 +283,29 @@ class FinanceAgent(BaseAgent):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Peer bus handler
|
# Peer bus handler
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'overdue_summary':
|
if request_type == 'overdue_summary':
|
||||||
partner_id = request.get('partner_id')
|
partner_id = params.get('partner_id')
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if partner_id:
|
if partner_id:
|
||||||
kwargs['partner_id'] = partner_id
|
kwargs['partner_id'] = partner_id
|
||||||
overdue = await self._ft.get_overdue_invoices(**kwargs)
|
overdue = await self._ft.get_overdue_invoices(**kwargs)
|
||||||
total = sum(inv.get('amount_residual', 0) for inv in overdue)
|
total = sum(inv.get('amount_residual', 0) for inv in overdue)
|
||||||
return {'overdue_count': len(overdue), 'overdue_total': total, 'invoices': overdue}
|
return {'overdue_count': len(overdue), 'overdue_total': total, 'invoices': overdue}
|
||||||
if req_type == 'payment_history':
|
if request_type == 'payment_history':
|
||||||
partner_id = request.get('partner_id')
|
partner_id = params.get('partner_id')
|
||||||
if not partner_id:
|
if not partner_id:
|
||||||
return {'error': 'partner_id required'}
|
return {'error': 'partner_id required'}
|
||||||
history = await self._ft.get_payment_history(partner_id=partner_id)
|
history = await self._ft.get_payment_history(partner_id=partner_id)
|
||||||
return {'history': history}
|
return {'history': history}
|
||||||
if req_type == 'financial_summary':
|
if request_type == 'financial_summary':
|
||||||
period = request.get('period', 'this_month')
|
period = params.get('period', 'this_month')
|
||||||
summary = await self._ft.get_financial_summary(period=period)
|
summary = await self._ft.get_financial_summary(period=period)
|
||||||
return {'summary': summary}
|
return {'summary': summary}
|
||||||
return {'error': f'Unknown peer request type: {req_type}'}
|
return {'error': f'Unknown peer request type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error('handle_peer_request failed type=%s: %s', req_type, exc)
|
logger.error('handle_peer_request failed type=%s: %s', request_type, exc)
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -115,15 +115,14 @@ class ProjectAgent(BaseAgent):
|
|||||||
raise ValueError(f'Unknown tool: {name}')
|
raise ValueError(f'Unknown tool: {name}')
|
||||||
return await dispatch[name](**args)
|
return await dispatch[name](**args)
|
||||||
|
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'project_list':
|
if request_type == 'project_list':
|
||||||
return {'projects': await self._pt.get_projects()}
|
return {'projects': await self._pt.get_projects()}
|
||||||
if req_type == 'task_count':
|
if request_type == 'task_count':
|
||||||
tasks = await self._pt.get_tasks(project_id=request.get('project_id'))
|
tasks = await self._pt.get_tasks(project_id=params.get('project_id'))
|
||||||
return {'count': len(tasks)}
|
return {'count': len(tasks)}
|
||||||
return {'error': f'Unknown type: {req_type}'}
|
return {'error': f'Unknown type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -119,14 +119,13 @@ class SalesAgent(BaseAgent):
|
|||||||
raise ValueError(f'Unknown tool: {name}')
|
raise ValueError(f'Unknown tool: {name}')
|
||||||
return await dispatch[name](**args)
|
return await dispatch[name](**args)
|
||||||
|
|
||||||
async def handle_peer_request(self, request: dict) -> dict:
|
async def handle_peer_request(self, request_type: str, params: dict, directive_id: str) -> dict:
|
||||||
req_type = request.get('type', '')
|
|
||||||
try:
|
try:
|
||||||
if req_type == 'sales_summary':
|
if request_type == 'sales_summary':
|
||||||
return await self._st.get_sales_summary()
|
return await self._st.get_sales_summary()
|
||||||
if req_type == 'customer_orders':
|
if request_type == 'customer_orders':
|
||||||
return {'orders': await self._st.get_customer_orders(partner_id=request['partner_id'])}
|
return {'orders': await self._st.get_customer_orders(partner_id=params['partner_id'])}
|
||||||
return {'error': f'Unknown type: {req_type}'}
|
return {'error': f'Unknown type: {request_type}'}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {'error': str(exc)}
|
return {'error': str(exc)}
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,15 @@ class OdooClient:
|
|||||||
await self._pg_pool.close()
|
await self._pg_pool.close()
|
||||||
|
|
||||||
async def _xmlrpc_call(self, proxy, method, *args):
|
async def _xmlrpc_call(self, proxy, method, *args):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
fn = getattr(proxy, method)
|
fn = getattr(proxy, method)
|
||||||
return await loop.run_in_executor(_executor, fn, *args)
|
try:
|
||||||
|
return await asyncio.wait_for(
|
||||||
|
loop.run_in_executor(_executor, fn, *args),
|
||||||
|
timeout=self._timeout,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise socket.timeout(f'Odoo XML-RPC timeout after {self._timeout}s')
|
||||||
|
|
||||||
async def _authenticate(self):
|
async def _authenticate(self):
|
||||||
async with self._auth_lock:
|
async with self._auth_lock:
|
||||||
|
|||||||
Reference in New Issue
Block a user