from __future__ import annotations import logging from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport from ..tools.crm_tools import CrmTools logger = logging.getLogger(__name__) CRM_TOOLS = [ {'name': 'get_leads', 'description': 'Get CRM leads', 'parameters': {'stage_id': {'type': 'integer', 'optional': True}, 'user_id': {'type': 'integer', 'optional': True}, 'limit': {'type': 'integer', 'optional': True}}}, {'name': 'get_opportunities', 'description': 'Get CRM opportunities', 'parameters': {'stage_id': {'type': 'integer', 'optional': True}, 'user_id': {'type': 'integer', 'optional': True}, 'limit': {'type': 'integer', 'optional': True}}}, {'name': 'get_pipeline_summary', 'description': 'Get pipeline summary by stage', 'parameters': {}}, {'name': 'update_lead_stage', 'description': 'Move a lead/opp to a new stage', 'parameters': {'lead_id': {'type': 'integer'}, 'stage_id': {'type': 'integer'}}}, {'name': 'assign_lead', 'description': 'Assign lead to a salesperson', 'parameters': {'lead_id': {'type': 'integer'}, 'user_id': {'type': 'integer'}}}, {'name': 'log_activity', 'description': 'Log a CRM activity', 'parameters': {'lead_id': {'type': 'integer'}, 'activity_type': {'type': 'string'}, 'note': {'type': 'string'}, 'date_deadline': {'type': 'string', 'optional': True}}}, {'name': 'get_won_lost_analysis', 'description': 'Get won/lost opportunity analysis', 'parameters': {'date_from': {'type': 'string', 'optional': True}, 'date_to': {'type': 'string', 'optional': True}}}, {'name': 'post_chatter_note', 'description': 'Post a note on a record', 'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'}, 'note': {'type': 'string'}}}, ] class CrmAgent(BaseAgent): name = 'crm_agent' domain = 'crm' required_odoo_module = 'crm' system_prompt_file = 'crm_system.txt' tools = CRM_TOOLS def __init__(self, odoo, llm, peer_bus=None): super().__init__(odoo, llm, peer_bus) self._ct = CrmTools(odoo) self._gathered_data = {} self._actions_taken = [] self._escalations_list = [] async def _plan(self, directive: AgentDirective) -> dict: intent = (directive.intent or '').lower() return { 'fetch_pipeline': any(k in intent for k in ('pipeline', 'summary', 'overview')), 'fetch_leads': 'lead' in intent, 'fetch_opportunities': any(k in intent for k in ('opportunit', 'deal')), 'fetch_won_lost': any(k in intent for k in ('won', 'lost', 'win rate')), 'user_id': directive.context.get('user_id'), } async def _gather(self, ctx: dict) -> dict: plan = ctx.get('plan', {}) data: dict = {} if plan.get('fetch_pipeline') or not any([plan.get('fetch_leads'), plan.get('fetch_opportunities')]): data['pipeline'] = await self._ct.get_pipeline_summary() if plan.get('fetch_leads'): data['leads'] = await self._ct.get_leads(user_id=plan.get('user_id'), limit=20) if plan.get('fetch_opportunities'): data['opportunities'] = await self._ct.get_opportunities(user_id=plan.get('user_id'), limit=20) if plan.get('fetch_won_lost'): data['won_lost'] = await self._ct.get_won_lost_analysis() self._gathered_data = data return data async def _reason(self, ctx: dict) -> dict: data = self._gathered_data analysis: dict = {'escalations': [], 'stale_leads': []} pipeline = data.get('pipeline', {}) if pipeline: weighted = pipeline.get('weighted_pipeline', 0) if weighted < 10000: analysis['escalations'].append(f'Low weighted pipeline: {weighted:.2f}') self._escalations_list = analysis['escalations'] return analysis async def _act(self, ctx: dict) -> list: return [] async def _report(self, ctx: dict) -> AgentReport: data = self._gathered_data parts = [] pipeline = data.get('pipeline', {}) if pipeline: total = pipeline.get('total_opportunities', 0) value = pipeline.get('weighted_pipeline', 0) parts.append(f'Pipeline: {total} opportunities, weighted value {value:.2f}.') won_lost = data.get('won_lost', {}) if won_lost: parts.append(f'Won: {won_lost.get("won_count", 0)}, Lost: {won_lost.get("lost_count", 0)}.') if not parts: parts.append('CRM review complete.') return AgentReport(agent=self.name, summary=chr(10).join(parts), data=data, escalations=self._escalations_list, actions_taken=[]) async def _dispatch_tool(self, name: str, args: dict): dispatch = { 'get_leads': self._ct.get_leads, 'get_opportunities': self._ct.get_opportunities, 'get_pipeline_summary': self._ct.get_pipeline_summary, 'update_lead_stage': self._ct.update_lead_stage, 'assign_lead': self._ct.assign_lead, 'log_activity': self._ct.log_activity, 'get_won_lost_analysis': self._ct.get_won_lost_analysis, 'post_chatter_note': self._ct.post_chatter_note, } if name not in dispatch: raise ValueError(f'Unknown tool: {name}') return await dispatch[name](**args) async def handle_peer_request(self, request: dict) -> dict: req_type = request.get('type', '') try: if req_type == 'pipeline_summary': return await self._ct.get_pipeline_summary() if req_type == 'opportunities': return {'opportunities': await self._ct.get_opportunities(user_id=request.get('user_id'))} return {'error': f'Unknown type: {req_type}'} except Exception as exc: return {'error': str(exc)} async def sweep(self) -> SweepReport: findings = [] try: pipeline = await self._ct.get_pipeline_summary() if pipeline.get('weighted_pipeline', 0) < 5000: findings.append({'type': 'low_pipeline', 'severity': 'medium', 'weighted': pipeline.get('weighted_pipeline', 0)}) except Exception as exc: return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc)) return SweepReport(agent=self.name, findings=findings, actions=[], summary=f'CRM sweep: {len(findings)} findings.')