feat(agents): add 7 specialist agents with tools and system prompts
Agents (all following 6-step contract: _plan/_gather/_reason/_act/_report): - AccountingAgent: trial balance, chart of accounts, tax summary (HIPAA-locked) - CrmAgent: pipeline summary, lead/opportunity management, won/lost analysis - SalesAgent: sales orders, quotations, revenue by rep, expired quote detection - ProjectAgent: task tracking, blocked/overdue detection, timesheet logging - ElearningAgent: course completion, low-engagement flagging, next-course suggestion - ExpensesAgent: expense sheets, pending approvals, policy violations (HIPAA-locked) - EmployeesAgent: headcount, contracts, leaves, attendance, expired contract sweep (HIPAA-locked) Tools (one file per domain): - accounting_tools.py, crm_tools.py, sales_tools.py, project_tools.py - elearning_tools.py, expenses_tools.py, employees_tools.py System prompts: each agent has a domain-specific system.txt with rules and output format All agents implement handle_peer_request() and sweep() for proactive monitoring HIPAA-locked agents (accounting, expenses, employees) enforced via LLMRouter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
147
agent_service/agents/accounting_agent.py
Normal file
147
agent_service/agents/accounting_agent.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport
|
||||
from ..tools.accounting_tools import AccountingTools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNTING_TOOLS = [
|
||||
{'name': 'get_journal_entries', 'description': 'Retrieve journal entries',
|
||||
'parameters': {'journal_id': {'type': 'integer', 'optional': True},
|
||||
'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True},
|
||||
'state': {'type': 'string', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_chart_of_accounts', 'description': 'Get chart of accounts',
|
||||
'parameters': {'account_type': {'type': 'string', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_account_balance', 'description': 'Get balance for a specific account',
|
||||
'parameters': {'account_id': {'type': 'integer'}}},
|
||||
{'name': 'get_trial_balance', 'description': 'Get trial balance for a period',
|
||||
'parameters': {'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True}}},
|
||||
{'name': 'get_tax_summary', 'description': 'Get tax summary for a period',
|
||||
'parameters': {'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True}}},
|
||||
{'name': 'flag_for_review', 'description': 'Flag a journal entry for review',
|
||||
'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'},
|
||||
'reason': {'type': 'string'},
|
||||
'severity': {'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 AccountingAgent(BaseAgent):
|
||||
name = 'accounting_agent'
|
||||
domain = 'accounting'
|
||||
required_odoo_module = 'account'
|
||||
system_prompt_file = 'accounting_system.txt'
|
||||
tools = ACCOUNTING_TOOLS
|
||||
|
||||
def __init__(self, odoo, llm, peer_bus=None):
|
||||
super().__init__(odoo, llm, peer_bus)
|
||||
self._at = AccountingTools(odoo)
|
||||
self._gathered_data = {}
|
||||
self._actions_taken = []
|
||||
self._escalations_list = []
|
||||
|
||||
async def _plan(self, directive: AgentDirective) -> dict:
|
||||
intent = (directive.intent or '').lower()
|
||||
return {
|
||||
'fetch_trial_balance': any(k in intent for k in ('trial', 'balance', 'report')),
|
||||
'fetch_tax': any(k in intent for k in ('tax', 'vat', 'gst')),
|
||||
'fetch_entries': any(k in intent for k in ('journal', 'entry', 'entries')),
|
||||
'date_from': directive.context.get('date_from'),
|
||||
'date_to': directive.context.get('date_to'),
|
||||
}
|
||||
|
||||
async def _gather(self, ctx: dict) -> dict:
|
||||
plan = ctx.get('plan', {})
|
||||
data: dict = {}
|
||||
if plan.get('fetch_trial_balance'):
|
||||
data['trial_balance'] = await self._at.get_trial_balance(
|
||||
date_from=plan.get('date_from'), date_to=plan.get('date_to'),
|
||||
)
|
||||
if plan.get('fetch_tax'):
|
||||
data['tax_summary'] = await self._at.get_tax_summary(
|
||||
date_from=plan.get('date_from'), date_to=plan.get('date_to'),
|
||||
)
|
||||
if plan.get('fetch_entries') or not data:
|
||||
data['entries'] = await self._at.get_journal_entries(limit=20)
|
||||
self._gathered_data = data
|
||||
return data
|
||||
|
||||
async def _reason(self, ctx: dict) -> dict:
|
||||
data = self._gathered_data
|
||||
analysis: dict = {'flags': [], 'escalations': []}
|
||||
trial = data.get('trial_balance', [])
|
||||
for account in trial:
|
||||
bal = account.get('balance', 0)
|
||||
if abs(bal) > 100000:
|
||||
analysis['flags'].append({'account': account.get('account_name'), 'balance': bal})
|
||||
self._escalations_list = analysis.get('escalations', [])
|
||||
return analysis
|
||||
|
||||
async def _act(self, ctx: dict) -> list:
|
||||
return []
|
||||
|
||||
async def _report(self, ctx: dict) -> AgentReport:
|
||||
data = self._gathered_data
|
||||
parts = []
|
||||
trial = data.get('trial_balance', [])
|
||||
if trial:
|
||||
parts.append(f'Trial balance: {len(trial)} accounts.')
|
||||
tax = data.get('tax_summary', {})
|
||||
if tax:
|
||||
parts.append(f'Tax: {tax.get("total_tax_amount", 0):.2f} in {tax.get("total_tax_lines", 0)} lines.')
|
||||
if not parts:
|
||||
parts.append('Accounting 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):
|
||||
if name == 'get_journal_entries':
|
||||
return await self._at.get_journal_entries(**args)
|
||||
if name == 'get_chart_of_accounts':
|
||||
return await self._at.get_chart_of_accounts(**args)
|
||||
if name == 'get_account_balance':
|
||||
return await self._at.get_account_balance(**args)
|
||||
if name == 'get_trial_balance':
|
||||
return await self._at.get_trial_balance(**args)
|
||||
if name == 'get_tax_summary':
|
||||
return await self._at.get_tax_summary(**args)
|
||||
if name == 'flag_for_review':
|
||||
return await self._at.flag_for_review(**args)
|
||||
if name == 'post_chatter_note':
|
||||
return await self._at.post_chatter_note(**args)
|
||||
raise ValueError(f'Unknown tool: {name}')
|
||||
|
||||
async def handle_peer_request(self, request: dict) -> dict:
|
||||
req_type = request.get('type', '')
|
||||
try:
|
||||
if req_type == 'trial_balance':
|
||||
return {'trial_balance': await self._at.get_trial_balance()}
|
||||
if req_type == 'account_balance':
|
||||
return await self._at.get_account_balance(account_id=request['account_id'])
|
||||
if req_type == 'tax_summary':
|
||||
return await self._at.get_tax_summary()
|
||||
return {'error': f'Unknown type: {req_type}'}
|
||||
except Exception as exc:
|
||||
return {'error': str(exc)}
|
||||
|
||||
async def sweep(self) -> SweepReport:
|
||||
findings = []
|
||||
try:
|
||||
trial = await self._at.get_trial_balance()
|
||||
for account in trial:
|
||||
if abs(account.get('balance', 0)) > 500000:
|
||||
findings.append({'type': 'large_balance', 'account': account.get('account_name'),
|
||||
'balance': account.get('balance', 0), 'severity': 'high'})
|
||||
except Exception as exc:
|
||||
return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc))
|
||||
return SweepReport(agent=self.name, findings=findings, actions=[],
|
||||
summary=f'Sweep: {len(findings)} large balance accounts found.')
|
||||
139
agent_service/agents/crm_agent.py
Normal file
139
agent_service/agents/crm_agent.py
Normal file
@@ -0,0 +1,139 @@
|
||||
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.')
|
||||
144
agent_service/agents/elearning_agent.py
Normal file
144
agent_service/agents/elearning_agent.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport
|
||||
from ..tools.elearning_tools import ElearningTools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ELEARNING_TOOLS = [
|
||||
{'name': 'get_courses', 'description': 'List eLearning courses',
|
||||
'parameters': {'active': {'type': 'boolean', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_course_stats', 'description': 'Get detailed stats for a course',
|
||||
'parameters': {'channel_id': {'type': 'integer'}}},
|
||||
{'name': 'get_enrolled_users', 'description': 'Get users enrolled in a course',
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_slide_completion', 'description': 'Get slide completion by user',
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
'min_completion': {'type': 'number', 'optional': True}}},
|
||||
{'name': 'get_learning_summary', 'description': 'Get overall learning summary', 'parameters': {}},
|
||||
{'name': 'flag_low_completion', 'description': 'Flag a course with low completion',
|
||||
'parameters': {'channel_id': {'type': 'integer'}, 'reason': {'type': 'string'}}},
|
||||
{'name': 'suggest_next_course', 'description': 'Suggest next course for a learner',
|
||||
'parameters': {'partner_id': {'type': 'integer'}}},
|
||||
{'name': 'post_chatter_note', 'description': 'Post a note on a record',
|
||||
'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'},
|
||||
'note': {'type': 'string'}}},
|
||||
]
|
||||
|
||||
|
||||
class ElearningAgent(BaseAgent):
|
||||
name = 'elearning_agent'
|
||||
domain = 'elearning'
|
||||
required_odoo_module = 'website_slides'
|
||||
system_prompt_file = 'elearning_system.txt'
|
||||
tools = ELEARNING_TOOLS
|
||||
|
||||
def __init__(self, odoo, llm, peer_bus=None):
|
||||
super().__init__(odoo, llm, peer_bus)
|
||||
self._el = ElearningTools(odoo)
|
||||
self._gathered_data = {}
|
||||
self._actions_taken = []
|
||||
self._escalations_list = []
|
||||
|
||||
async def _plan(self, directive: AgentDirective) -> dict:
|
||||
intent = (directive.intent or '').lower()
|
||||
return {
|
||||
'fetch_summary': any(k in intent for k in ('summary', 'overview', 'learning')),
|
||||
'fetch_courses': 'course' in intent,
|
||||
'channel_id': directive.context.get('channel_id'),
|
||||
'partner_id': directive.context.get('partner_id'),
|
||||
}
|
||||
|
||||
async def _gather(self, ctx: dict) -> dict:
|
||||
plan = ctx.get('plan', {})
|
||||
data: dict = {}
|
||||
data['summary'] = await self._el.get_learning_summary()
|
||||
if plan.get('fetch_courses') or plan.get('channel_id'):
|
||||
if plan.get('channel_id'):
|
||||
data['course_stats'] = await self._el.get_course_stats(channel_id=plan['channel_id'])
|
||||
else:
|
||||
data['courses'] = await self._el.get_courses(limit=20)
|
||||
self._gathered_data = data
|
||||
return data
|
||||
|
||||
async def _reason(self, ctx: dict) -> dict:
|
||||
data = self._gathered_data
|
||||
analysis: dict = {'escalations': [], 'low_completion': []}
|
||||
summary = data.get('summary', {})
|
||||
low = summary.get('low_completion_courses', [])
|
||||
analysis['low_completion'] = low
|
||||
if len(low) > 3:
|
||||
analysis['escalations'].append(f'{len(low)} courses have <30% completion rate.')
|
||||
self._escalations_list = analysis['escalations']
|
||||
return analysis
|
||||
|
||||
async def _act(self, ctx: dict) -> list:
|
||||
actions = []
|
||||
analysis = ctx.get('analysis', {})
|
||||
for course in analysis.get('low_completion', [])[:3]:
|
||||
try:
|
||||
await self._el.flag_low_completion(
|
||||
channel_id=course.get('id'),
|
||||
reason=f'Completion rate {course.get("completion_rate", 0):.1f}% is below 30% threshold',
|
||||
)
|
||||
actions.append({'action': 'flag_low_completion', 'course_id': course.get('id'), 'success': True})
|
||||
except Exception as exc:
|
||||
logger.warning('flag_low_completion failed: %s', exc)
|
||||
self._actions_taken = actions
|
||||
return actions
|
||||
|
||||
async def _report(self, ctx: dict) -> AgentReport:
|
||||
data = self._gathered_data
|
||||
summary = data.get('summary', {})
|
||||
parts = []
|
||||
if summary:
|
||||
parts.append(
|
||||
f'eLearning: {summary.get("total_courses", 0)} courses, '
|
||||
f'{summary.get("total_enrollments", 0)} enrollments, '
|
||||
f'{summary.get("avg_completion", 0):.1f}% avg completion.'
|
||||
)
|
||||
if not parts:
|
||||
parts.append('eLearning review complete.')
|
||||
return AgentReport(agent=self.name, summary=chr(10).join(parts),
|
||||
data=data, escalations=self._escalations_list, actions_taken=self._actions_taken)
|
||||
|
||||
async def _dispatch_tool(self, name: str, args: dict):
|
||||
dispatch = {
|
||||
'get_courses': self._el.get_courses,
|
||||
'get_course_stats': self._el.get_course_stats,
|
||||
'get_enrolled_users': self._el.get_enrolled_users,
|
||||
'get_slide_completion': self._el.get_slide_completion,
|
||||
'get_learning_summary': self._el.get_learning_summary,
|
||||
'flag_low_completion': self._el.flag_low_completion,
|
||||
'suggest_next_course': self._el.suggest_next_course,
|
||||
'post_chatter_note': self._el.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 == 'learning_summary':
|
||||
return await self._el.get_learning_summary()
|
||||
if req_type == 'suggest_courses':
|
||||
return {'courses': await self._el.suggest_next_course(partner_id=request['partner_id'])}
|
||||
return {'error': f'Unknown type: {req_type}'}
|
||||
except Exception as exc:
|
||||
return {'error': str(exc)}
|
||||
|
||||
async def sweep(self) -> SweepReport:
|
||||
findings = []
|
||||
try:
|
||||
summary = await self._el.get_learning_summary()
|
||||
for course in summary.get('low_completion_courses', []):
|
||||
findings.append({'type': 'low_completion', 'course_id': course.get('id'),
|
||||
'name': course.get('name'), 'completion': course.get('completion_rate', 0),
|
||||
'severity': 'medium'})
|
||||
except Exception as exc:
|
||||
return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc))
|
||||
return SweepReport(agent=self.name, findings=findings, actions=[],
|
||||
summary=f'eLearning sweep: {len(findings)} low-completion courses.')
|
||||
158
agent_service/agents/employees_agent.py
Normal file
158
agent_service/agents/employees_agent.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport
|
||||
from ..tools.employees_tools import EmployeesTools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EMPLOYEES_TOOLS = [
|
||||
{'name': 'get_employees', 'description': 'List employees',
|
||||
'parameters': {'department_id': {'type': 'integer', 'optional': True},
|
||||
'active': {'type': 'boolean', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_employee_profile', 'description': 'Get detailed profile for one employee',
|
||||
'parameters': {'employee_id': {'type': 'integer'}}},
|
||||
{'name': 'get_leaves', 'description': 'Get leave requests',
|
||||
'parameters': {'employee_id': {'type': 'integer', 'optional': True},
|
||||
'state': {'type': 'string', 'optional': True},
|
||||
'date_from': {'type': 'string', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_contracts', 'description': 'Get employee contracts',
|
||||
'parameters': {'employee_id': {'type': 'integer', 'optional': True},
|
||||
'state': {'type': 'string', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_attendance_summary', 'description': 'Get attendance summary for an employee',
|
||||
'parameters': {'employee_id': {'type': 'integer'},
|
||||
'date_from': {'type': 'string'},
|
||||
'date_to': {'type': 'string'}}},
|
||||
{'name': 'get_department_summary', 'description': 'Get headcount and contract summary for a department',
|
||||
'parameters': {'department_id': {'type': 'integer'}}},
|
||||
{'name': 'flag_for_review', 'description': 'Flag a record for review',
|
||||
'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'},
|
||||
'reason': {'type': 'string'},
|
||||
'severity': {'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 EmployeesAgent(BaseAgent):
|
||||
name = 'employees_agent'
|
||||
domain = 'employees'
|
||||
required_odoo_module = 'hr'
|
||||
system_prompt_file = 'employees_system.txt'
|
||||
tools = EMPLOYEES_TOOLS
|
||||
|
||||
def __init__(self, odoo, llm, peer_bus=None):
|
||||
super().__init__(odoo, llm, peer_bus)
|
||||
self._ht = EmployeesTools(odoo)
|
||||
self._gathered_data = {}
|
||||
self._actions_taken = []
|
||||
self._escalations_list = []
|
||||
|
||||
async def _plan(self, directive: AgentDirective) -> dict:
|
||||
intent = (directive.intent or '').lower()
|
||||
return {
|
||||
'fetch_employees': any(k in intent for k in ('employee', 'headcount', 'staff')),
|
||||
'fetch_leaves': any(k in intent for k in ('leave', 'absence', 'holiday', 'vacation')),
|
||||
'fetch_contracts': 'contract' in intent,
|
||||
'department_id': directive.context.get('department_id'),
|
||||
'employee_id': directive.context.get('employee_id'),
|
||||
}
|
||||
|
||||
async def _gather(self, ctx: dict) -> dict:
|
||||
plan = ctx.get('plan', {})
|
||||
data: dict = {}
|
||||
if plan.get('fetch_employees') or not any([plan.get('fetch_leaves'), plan.get('fetch_contracts')]):
|
||||
if plan.get('department_id'):
|
||||
data['dept_summary'] = await self._ht.get_department_summary(plan['department_id'])
|
||||
else:
|
||||
data['employees'] = await self._ht.get_employees(limit=50)
|
||||
if plan.get('fetch_leaves'):
|
||||
data['leaves'] = await self._ht.get_leaves(
|
||||
employee_id=plan.get('employee_id'), state='validate1', limit=20,
|
||||
)
|
||||
if plan.get('fetch_contracts'):
|
||||
data['contracts'] = await self._ht.get_contracts(
|
||||
employee_id=plan.get('employee_id'), limit=20,
|
||||
)
|
||||
self._gathered_data = data
|
||||
return data
|
||||
|
||||
async def _reason(self, ctx: dict) -> dict:
|
||||
data = self._gathered_data
|
||||
analysis: dict = {'escalations': []}
|
||||
contracts = data.get('contracts', [])
|
||||
import datetime
|
||||
today = str(datetime.date.today())
|
||||
expiring = [c for c in contracts if c.get('date_end') and c['date_end'] < today]
|
||||
if expiring:
|
||||
analysis['escalations'].append(f'{len(expiring)} contracts have expired.')
|
||||
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 = []
|
||||
employees = data.get('employees', [])
|
||||
if employees:
|
||||
parts.append(f'Employees: {len(employees)} active.')
|
||||
dept = data.get('dept_summary', {})
|
||||
if dept:
|
||||
parts.append(f'Department headcount: {dept.get("headcount", 0)}, avg wage: {dept.get("avg_wage", 0):.2f}.')
|
||||
leaves = data.get('leaves', [])
|
||||
if leaves:
|
||||
parts.append(f'Pending leave approvals: {len(leaves)}.')
|
||||
if not parts:
|
||||
parts.append('HR 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_employees': self._ht.get_employees,
|
||||
'get_employee_profile': self._ht.get_employee_profile,
|
||||
'get_leaves': self._ht.get_leaves,
|
||||
'get_contracts': self._ht.get_contracts,
|
||||
'get_attendance_summary': self._ht.get_attendance_summary,
|
||||
'get_department_summary': self._ht.get_department_summary,
|
||||
'flag_for_review': self._ht.flag_for_review,
|
||||
'post_chatter_note': self._ht.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 == 'employee_list':
|
||||
return {'employees': await self._ht.get_employees(department_id=request.get('department_id'))}
|
||||
if req_type == 'employee_profile':
|
||||
return await self._ht.get_employee_profile(employee_id=request['employee_id'])
|
||||
if req_type == 'headcount':
|
||||
employees = await self._ht.get_employees(department_id=request.get('department_id'))
|
||||
return {'headcount': len(employees)}
|
||||
return {'error': f'Unknown type: {req_type}'}
|
||||
except Exception as exc:
|
||||
return {'error': str(exc)}
|
||||
|
||||
async def sweep(self) -> SweepReport:
|
||||
findings = []
|
||||
try:
|
||||
import datetime
|
||||
today = str(datetime.date.today())
|
||||
contracts = await self._ht.get_contracts(state='open', limit=200)
|
||||
for c in contracts:
|
||||
if c.get('date_end') and c['date_end'] < today:
|
||||
findings.append({'type': 'expired_contract', 'contract_id': c.get('id'),
|
||||
'employee': c.get('employee_id', [0, ''])[1] if isinstance(c.get('employee_id'), list) else '',
|
||||
'expired': c.get('date_end'), 'severity': 'high'})
|
||||
except Exception as exc:
|
||||
return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc))
|
||||
return SweepReport(agent=self.name, findings=findings, actions=[],
|
||||
summary=f'HR sweep: {len(findings)} expired contracts found.')
|
||||
140
agent_service/agents/expenses_agent.py
Normal file
140
agent_service/agents/expenses_agent.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport
|
||||
from ..tools.expenses_tools import ExpensesTools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXPENSES_TOOLS = [
|
||||
{'name': 'get_expenses', 'description': 'Retrieve expense records',
|
||||
'parameters': {'employee_id': {'type': 'integer', 'optional': True},
|
||||
'state': {'type': 'string', 'optional': True},
|
||||
'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_expense_sheets', 'description': 'Get expense report sheets',
|
||||
'parameters': {'state': {'type': 'string', 'optional': True},
|
||||
'employee_id': {'type': 'integer', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_pending_approvals', 'description': 'Get expense sheets pending approval',
|
||||
'parameters': {}},
|
||||
{'name': 'approve_expense_sheet', 'description': 'Approve an expense sheet',
|
||||
'parameters': {'sheet_id': {'type': 'integer'}}},
|
||||
{'name': 'get_expenses_summary', 'description': 'Get expense summary for a period',
|
||||
'parameters': {'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True}}},
|
||||
{'name': 'get_expense_by_employee', 'description': 'Get expenses for a specific employee',
|
||||
'parameters': {'employee_id': {'type': 'integer'},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'flag_for_review', 'description': 'Flag an expense for review',
|
||||
'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'},
|
||||
'reason': {'type': 'string'},
|
||||
'severity': {'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 ExpensesAgent(BaseAgent):
|
||||
name = 'expenses_agent'
|
||||
domain = 'expenses'
|
||||
required_odoo_module = 'hr_expense'
|
||||
system_prompt_file = 'expenses_system.txt'
|
||||
tools = EXPENSES_TOOLS
|
||||
|
||||
def __init__(self, odoo, llm, peer_bus=None):
|
||||
super().__init__(odoo, llm, peer_bus)
|
||||
self._et = ExpensesTools(odoo)
|
||||
self._gathered_data = {}
|
||||
self._actions_taken = []
|
||||
self._escalations_list = []
|
||||
|
||||
async def _plan(self, directive: AgentDirective) -> dict:
|
||||
intent = (directive.intent or '').lower()
|
||||
return {
|
||||
'fetch_summary': any(k in intent for k in ('summary', 'overview', 'report')),
|
||||
'fetch_pending': any(k in intent for k in ('pending', 'approve', 'approval')),
|
||||
'employee_id': directive.context.get('employee_id'),
|
||||
'date_from': directive.context.get('date_from'),
|
||||
'date_to': directive.context.get('date_to'),
|
||||
}
|
||||
|
||||
async def _gather(self, ctx: dict) -> dict:
|
||||
plan = ctx.get('plan', {})
|
||||
data: dict = {}
|
||||
data['summary'] = await self._et.get_expenses_summary(
|
||||
date_from=plan.get('date_from'), date_to=plan.get('date_to'),
|
||||
)
|
||||
if plan.get('fetch_pending'):
|
||||
data['pending'] = await self._et.get_pending_approvals()
|
||||
self._gathered_data = data
|
||||
return data
|
||||
|
||||
async def _reason(self, ctx: dict) -> dict:
|
||||
data = self._gathered_data
|
||||
analysis: dict = {'escalations': [], 'flags': []}
|
||||
summary = data.get('summary', {})
|
||||
if summary.get('pending_approval_count', 0) > 10:
|
||||
analysis['escalations'].append(
|
||||
f'{summary["pending_approval_count"]} expense sheets pending approval.'
|
||||
)
|
||||
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
|
||||
summary = data.get('summary', {})
|
||||
parts = []
|
||||
if summary:
|
||||
parts.append(
|
||||
f'Expenses: {summary.get("total_expenses", 0)} records, '
|
||||
f'total {summary.get("total_amount", 0):.2f}. '
|
||||
f'{summary.get("pending_approval_count", 0)} pending approval.'
|
||||
)
|
||||
if not parts:
|
||||
parts.append('Expenses 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_expenses': self._et.get_expenses,
|
||||
'get_expense_sheets': self._et.get_expense_sheets,
|
||||
'get_pending_approvals': self._et.get_pending_approvals,
|
||||
'approve_expense_sheet': self._et.approve_expense_sheet,
|
||||
'get_expenses_summary': self._et.get_expenses_summary,
|
||||
'get_expense_by_employee': self._et.get_expense_by_employee,
|
||||
'flag_for_review': self._et.flag_for_review,
|
||||
'post_chatter_note': self._et.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 == 'expenses_summary':
|
||||
return await self._et.get_expenses_summary()
|
||||
if req_type == 'employee_expenses':
|
||||
return {'expenses': await self._et.get_expense_by_employee(employee_id=request['employee_id'])}
|
||||
return {'error': f'Unknown type: {req_type}'}
|
||||
except Exception as exc:
|
||||
return {'error': str(exc)}
|
||||
|
||||
async def sweep(self) -> SweepReport:
|
||||
findings = []
|
||||
try:
|
||||
pending = await self._et.get_pending_approvals()
|
||||
for sheet in pending:
|
||||
findings.append({'type': 'pending_expense_approval', 'sheet_id': sheet.get('id'),
|
||||
'employee': sheet.get('employee_id', [0, ''])[1] if isinstance(sheet.get('employee_id'), list) else '',
|
||||
'amount': sheet.get('total_amount', 0), 'severity': 'low'})
|
||||
except Exception as exc:
|
||||
return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc))
|
||||
return SweepReport(agent=self.name, findings=findings, actions=[],
|
||||
summary=f'Expenses sweep: {len(findings)} pending approvals.')
|
||||
146
agent_service/agents/project_agent.py
Normal file
146
agent_service/agents/project_agent.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport
|
||||
from ..tools.project_tools import ProjectTools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROJECT_TOOLS = [
|
||||
{'name': 'get_projects', 'description': 'List projects',
|
||||
'parameters': {'active': {'type': 'boolean', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_tasks', 'description': 'Get tasks with filters',
|
||||
'parameters': {'project_id': {'type': 'integer', 'optional': True},
|
||||
'stage_id': {'type': 'integer', 'optional': True},
|
||||
'user_id': {'type': 'integer', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_project_summary', 'description': 'Get summary for a specific project',
|
||||
'parameters': {'project_id': {'type': 'integer'}}},
|
||||
{'name': 'update_task_stage', 'description': 'Move task to a new stage',
|
||||
'parameters': {'task_id': {'type': 'integer'}, 'stage_id': {'type': 'integer'}}},
|
||||
{'name': 'assign_task', 'description': 'Assign task to a user',
|
||||
'parameters': {'task_id': {'type': 'integer'}, 'user_id': {'type': 'integer'}}},
|
||||
{'name': 'create_task', 'description': 'Create a new task in a project',
|
||||
'parameters': {'project_id': {'type': 'integer'}, 'name': {'type': 'string'},
|
||||
'description': {'type': 'string', 'optional': True},
|
||||
'user_id': {'type': 'integer', 'optional': True},
|
||||
'date_deadline': {'type': 'string', 'optional': True}}},
|
||||
{'name': 'log_timesheet', 'description': 'Log timesheet hours on a task',
|
||||
'parameters': {'task_id': {'type': 'integer'}, 'employee_id': {'type': 'integer'},
|
||||
'hours': {'type': 'number'}, 'description': {'type': 'string', 'optional': True},
|
||||
'date': {'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 ProjectAgent(BaseAgent):
|
||||
name = 'project_agent'
|
||||
domain = 'project'
|
||||
required_odoo_module = 'project'
|
||||
system_prompt_file = 'project_system.txt'
|
||||
tools = PROJECT_TOOLS
|
||||
|
||||
def __init__(self, odoo, llm, peer_bus=None):
|
||||
super().__init__(odoo, llm, peer_bus)
|
||||
self._pt = ProjectTools(odoo)
|
||||
self._gathered_data = {}
|
||||
self._actions_taken = []
|
||||
self._escalations_list = []
|
||||
|
||||
async def _plan(self, directive: AgentDirective) -> dict:
|
||||
intent = (directive.intent or '').lower()
|
||||
return {
|
||||
'fetch_projects': any(k in intent for k in ('project', 'overview')),
|
||||
'fetch_tasks': 'task' in intent,
|
||||
'project_id': directive.context.get('project_id'),
|
||||
'user_id': directive.context.get('user_id'),
|
||||
}
|
||||
|
||||
async def _gather(self, ctx: dict) -> dict:
|
||||
plan = ctx.get('plan', {})
|
||||
data: dict = {}
|
||||
if plan.get('fetch_projects') or not plan.get('fetch_tasks'):
|
||||
data['projects'] = await self._pt.get_projects(limit=20)
|
||||
if plan.get('fetch_tasks') or plan.get('project_id'):
|
||||
data['tasks'] = await self._pt.get_tasks(
|
||||
project_id=plan.get('project_id'), user_id=plan.get('user_id'), limit=50,
|
||||
)
|
||||
self._gathered_data = data
|
||||
return data
|
||||
|
||||
async def _reason(self, ctx: dict) -> dict:
|
||||
data = self._gathered_data
|
||||
analysis: dict = {'escalations': [], 'blocked_tasks': []}
|
||||
tasks = data.get('tasks', [])
|
||||
blocked = [t for t in tasks if t.get('kanban_state') == 'blocked']
|
||||
analysis['blocked_tasks'] = blocked
|
||||
if len(blocked) > 5:
|
||||
analysis['escalations'].append(f'{len(blocked)} tasks are blocked.')
|
||||
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
|
||||
analysis = ctx.get('analysis', {})
|
||||
parts = []
|
||||
projects = data.get('projects', [])
|
||||
if projects:
|
||||
parts.append(f'Projects: {len(projects)} active.')
|
||||
tasks = data.get('tasks', [])
|
||||
if tasks:
|
||||
blocked = len(analysis.get('blocked_tasks', []))
|
||||
parts.append(f'Tasks: {len(tasks)} total, {blocked} blocked.')
|
||||
if not parts:
|
||||
parts.append('Project 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_projects': self._pt.get_projects,
|
||||
'get_tasks': self._pt.get_tasks,
|
||||
'get_project_summary': self._pt.get_project_summary,
|
||||
'update_task_stage': self._pt.update_task_stage,
|
||||
'assign_task': self._pt.assign_task,
|
||||
'create_task': self._pt.create_task,
|
||||
'log_timesheet': self._pt.log_timesheet,
|
||||
'post_chatter_note': self._pt.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 == 'project_list':
|
||||
return {'projects': await self._pt.get_projects()}
|
||||
if req_type == 'task_count':
|
||||
tasks = await self._pt.get_tasks(project_id=request.get('project_id'))
|
||||
return {'count': len(tasks)}
|
||||
return {'error': f'Unknown type: {req_type}'}
|
||||
except Exception as exc:
|
||||
return {'error': str(exc)}
|
||||
|
||||
async def sweep(self) -> SweepReport:
|
||||
findings = []
|
||||
try:
|
||||
tasks = await self._pt.get_tasks(limit=200)
|
||||
import datetime
|
||||
today = str(datetime.date.today())
|
||||
for t in tasks:
|
||||
if t.get('kanban_state') == 'blocked':
|
||||
findings.append({'type': 'blocked_task', 'task_id': t.get('id'),
|
||||
'name': t.get('name', ''), 'severity': 'medium'})
|
||||
elif t.get('date_deadline') and t['date_deadline'] < today:
|
||||
findings.append({'type': 'overdue_task', 'task_id': t.get('id'),
|
||||
'name': t.get('name', ''), 'severity': 'low'})
|
||||
except Exception as exc:
|
||||
return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc))
|
||||
return SweepReport(agent=self.name, findings=findings, actions=[],
|
||||
summary=f'Project sweep: {len(findings)} issues found.')
|
||||
147
agent_service/agents/sales_agent.py
Normal file
147
agent_service/agents/sales_agent.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from .base_agent import BaseAgent, AgentReport, AgentDirective, SweepReport
|
||||
from ..tools.sales_tools import SalesTools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SALES_TOOLS = [
|
||||
{'name': 'get_sales_orders', 'description': 'Retrieve confirmed sales orders',
|
||||
'parameters': {'state': {'type': 'string', 'optional': True},
|
||||
'partner_id': {'type': 'integer', 'optional': True},
|
||||
'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_quotations', 'description': 'Get open quotations',
|
||||
'parameters': {'partner_id': {'type': 'integer', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_sales_summary', 'description': 'Get sales summary and rep breakdown',
|
||||
'parameters': {'date_from': {'type': 'string', 'optional': True},
|
||||
'date_to': {'type': 'string', 'optional': True}}},
|
||||
{'name': 'get_customer_orders', 'description': 'Get all orders for a specific customer',
|
||||
'parameters': {'partner_id': {'type': 'integer'},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'confirm_quotation', 'description': 'Confirm a draft quotation to sales order',
|
||||
'parameters': {'order_id': {'type': 'integer'}}},
|
||||
{'name': 'update_order_note', 'description': 'Update the internal note on an order',
|
||||
'parameters': {'order_id': {'type': 'integer'}, 'note': {'type': 'string'}}},
|
||||
{'name': 'flag_for_review', 'description': 'Flag a sales order for review',
|
||||
'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'},
|
||||
'reason': {'type': 'string'},
|
||||
'severity': {'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 SalesAgent(BaseAgent):
|
||||
name = 'sales_agent'
|
||||
domain = 'sales'
|
||||
required_odoo_module = 'sale'
|
||||
system_prompt_file = 'sales_system.txt'
|
||||
tools = SALES_TOOLS
|
||||
|
||||
def __init__(self, odoo, llm, peer_bus=None):
|
||||
super().__init__(odoo, llm, peer_bus)
|
||||
self._st = SalesTools(odoo)
|
||||
self._gathered_data = {}
|
||||
self._actions_taken = []
|
||||
self._escalations_list = []
|
||||
|
||||
async def _plan(self, directive: AgentDirective) -> dict:
|
||||
intent = (directive.intent or '').lower()
|
||||
return {
|
||||
'fetch_summary': any(k in intent for k in ('summary', 'overview', 'report', 'revenue')),
|
||||
'fetch_quotations': any(k in intent for k in ('quotation', 'quote', 'draft')),
|
||||
'fetch_orders': any(k in intent for k in ('order', 'sale')),
|
||||
'partner_id': directive.context.get('partner_id'),
|
||||
'date_from': directive.context.get('date_from'),
|
||||
'date_to': directive.context.get('date_to'),
|
||||
}
|
||||
|
||||
async def _gather(self, ctx: dict) -> dict:
|
||||
plan = ctx.get('plan', {})
|
||||
data: dict = {}
|
||||
if plan.get('fetch_summary') or not any([plan.get('fetch_quotations'), plan.get('fetch_orders')]):
|
||||
data['summary'] = await self._st.get_sales_summary(
|
||||
date_from=plan.get('date_from'), date_to=plan.get('date_to'),
|
||||
)
|
||||
if plan.get('fetch_quotations'):
|
||||
data['quotations'] = await self._st.get_quotations(
|
||||
partner_id=plan.get('partner_id'), limit=20,
|
||||
)
|
||||
if plan.get('fetch_orders'):
|
||||
data['orders'] = await self._st.get_sales_orders(
|
||||
partner_id=plan.get('partner_id'), limit=20,
|
||||
)
|
||||
self._gathered_data = data
|
||||
return data
|
||||
|
||||
async def _reason(self, ctx: dict) -> dict:
|
||||
data = self._gathered_data
|
||||
analysis: dict = {'escalations': []}
|
||||
summary = data.get('summary', {})
|
||||
if summary and summary.get('total_revenue', 0) == 0:
|
||||
analysis['escalations'].append('No confirmed sales orders in the period.')
|
||||
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 = []
|
||||
summary = data.get('summary', {})
|
||||
if summary:
|
||||
parts.append(
|
||||
f'Sales: {summary.get("order_count", 0)} orders, '
|
||||
f'total revenue {summary.get("total_revenue", 0):.2f}.'
|
||||
)
|
||||
if not parts:
|
||||
parts.append('Sales 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_sales_orders': self._st.get_sales_orders,
|
||||
'get_quotations': self._st.get_quotations,
|
||||
'get_sales_summary': self._st.get_sales_summary,
|
||||
'get_customer_orders': self._st.get_customer_orders,
|
||||
'confirm_quotation': self._st.confirm_quotation,
|
||||
'update_order_note': self._st.update_order_note,
|
||||
'flag_for_review': self._st.flag_for_review,
|
||||
'post_chatter_note': self._st.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 == 'sales_summary':
|
||||
return await self._st.get_sales_summary()
|
||||
if req_type == 'customer_orders':
|
||||
return {'orders': await self._st.get_customer_orders(partner_id=request['partner_id'])}
|
||||
return {'error': f'Unknown type: {req_type}'}
|
||||
except Exception as exc:
|
||||
return {'error': str(exc)}
|
||||
|
||||
async def sweep(self) -> SweepReport:
|
||||
findings = []
|
||||
try:
|
||||
quotations = await self._st.get_quotations(limit=50)
|
||||
import datetime
|
||||
today = str(datetime.date.today())
|
||||
expired = [q for q in quotations if q.get('validity_date') and q['validity_date'] < today]
|
||||
for q in expired:
|
||||
findings.append({'type': 'expired_quotation', 'order_id': q.get('id'),
|
||||
'partner': q.get('partner_id', [0, 'Unknown'])[1] if isinstance(q.get('partner_id'), list) else '',
|
||||
'severity': 'low'})
|
||||
except Exception as exc:
|
||||
return SweepReport(agent=self.name, findings=[], actions=[], error=str(exc))
|
||||
return SweepReport(agent=self.name, findings=findings, actions=[],
|
||||
summary=f'Sales sweep: {len(findings)} expired quotations found.')
|
||||
Reference in New Issue
Block a user