update
This commit is contained in:
6
fusion_clock_ai/controllers/__init__.py
Normal file
6
fusion_clock_ai/controllers/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import ai_api
|
||||
from . import portal_ai
|
||||
BIN
fusion_clock_ai/controllers/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
fusion_clock_ai/controllers/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_clock_ai/controllers/__pycache__/ai_api.cpython-312.pyc
Normal file
BIN
fusion_clock_ai/controllers/__pycache__/ai_api.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
216
fusion_clock_ai/controllers/ai_api.py
Normal file
216
fusion_clock_ai/controllers/ai_api.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
from datetime import datetime, timedelta, date
|
||||
from odoo import http, fields as odoo_fields, _
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionClockAIAPI(http.Controller):
|
||||
|
||||
def _get_employee(self):
|
||||
return request.env['hr.employee'].sudo().search([
|
||||
('user_id', '=', request.env.uid),
|
||||
], limit=1)
|
||||
|
||||
@http.route('/fusion_clock_ai/manager_chat', type='json', auth='user')
|
||||
def manager_chat(self, message, conversation_id=None, date_from=None, date_to=None):
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
return {'error': 'Access denied. Manager role required.'}
|
||||
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
if ICP.get_param('fusion_clock_ai.enable_manager_chat', 'True') != 'True':
|
||||
return {'error': 'Manager AI Chat is disabled in settings.'}
|
||||
|
||||
AI = request.env['fusion.clock.ai.service'].sudo()
|
||||
|
||||
if not date_from:
|
||||
date_from = (date.today() - timedelta(days=7)).isoformat()
|
||||
if not date_to:
|
||||
date_to = date.today().isoformat()
|
||||
|
||||
d_from = date.fromisoformat(date_from)
|
||||
d_to = date.fromisoformat(date_to)
|
||||
|
||||
employees = request.env['hr.employee'].sudo().search([
|
||||
('x_fclk_enable_clock', '=', True),
|
||||
('company_id', '=', request.env.company.id),
|
||||
])
|
||||
team_context = AI._build_team_context(employees, d_from, d_to)
|
||||
|
||||
Conversation = request.env['fusion.clock.ai.conversation'].sudo()
|
||||
if conversation_id:
|
||||
conv = Conversation.browse(int(conversation_id))
|
||||
if not conv.exists() or conv.user_id.id != user.id:
|
||||
conv = Conversation.create({
|
||||
'user_id': user.id,
|
||||
'conversation_type': 'manager_query',
|
||||
})
|
||||
else:
|
||||
conv = Conversation.create({
|
||||
'user_id': user.id,
|
||||
'conversation_type': 'manager_query',
|
||||
})
|
||||
|
||||
Message = request.env['fusion.clock.ai.message'].sudo()
|
||||
Message.create({
|
||||
'conversation_id': conv.id,
|
||||
'role': 'user',
|
||||
'content': message,
|
||||
})
|
||||
|
||||
system_prompt = AI._get_system_prompt('manager_query')
|
||||
messages = [
|
||||
{'role': 'system', 'content': f"{system_prompt}\n\n--- ATTENDANCE DATA ---\n{team_context}"},
|
||||
]
|
||||
for msg in conv.message_ids:
|
||||
messages.append({'role': msg.role, 'content': msg.content})
|
||||
|
||||
try:
|
||||
response = AI.chat_completion(messages, feature='manager_query')
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
Message.create({
|
||||
'conversation_id': conv.id,
|
||||
'role': 'assistant',
|
||||
'content': response,
|
||||
})
|
||||
|
||||
return {
|
||||
'response': response,
|
||||
'conversation_id': conv.id,
|
||||
}
|
||||
|
||||
@http.route('/fusion_clock_ai/run_analysis', type='json', auth='user')
|
||||
def run_analysis(self, analysis_type, date_from=None, date_to=None, employee_id=None):
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
return {'error': 'Access denied.'}
|
||||
|
||||
AI = request.env['fusion.clock.ai.service'].sudo()
|
||||
|
||||
if not date_from:
|
||||
date_from = (date.today() - timedelta(days=7)).isoformat()
|
||||
if not date_to:
|
||||
date_to = date.today().isoformat()
|
||||
d_from = date.fromisoformat(date_from)
|
||||
d_to = date.fromisoformat(date_to)
|
||||
|
||||
prompt_map = {
|
||||
'anomaly': 'anomaly_detection',
|
||||
'understaffing': 'understaffing_prediction',
|
||||
'shift': 'shift_optimization',
|
||||
'compliance': 'compliance_check',
|
||||
'geofence': 'geofence_tuning',
|
||||
}
|
||||
prompt_key = prompt_map.get(analysis_type)
|
||||
if not prompt_key:
|
||||
return {'error': f'Unknown analysis type: {analysis_type}'}
|
||||
|
||||
employees = request.env['hr.employee'].sudo().search([
|
||||
('x_fclk_enable_clock', '=', True),
|
||||
('company_id', '=', request.env.company.id),
|
||||
])
|
||||
context_data = AI._build_team_context(employees, d_from, d_to)
|
||||
|
||||
if analysis_type == 'geofence':
|
||||
context_data += "\n\n" + self._build_geofence_context(d_from, d_to)
|
||||
|
||||
system_prompt = AI._get_system_prompt(prompt_key)
|
||||
|
||||
cache_key = AI._get_cache_key(prompt_key, hashlib.sha256(
|
||||
f"{d_from}:{d_to}:{request.env.company.id}".encode()
|
||||
).hexdigest())
|
||||
|
||||
try:
|
||||
response = AI.chat_completion(
|
||||
[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': context_data},
|
||||
],
|
||||
feature=analysis_type,
|
||||
cache_key=cache_key,
|
||||
)
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
return {'response': response}
|
||||
|
||||
def _build_geofence_context(self, date_from, date_to):
|
||||
Log = request.env['fusion.clock.activity.log'].sudo()
|
||||
locations = request.env['fusion.clock.location'].sudo().search([
|
||||
('active', '=', True),
|
||||
('company_id', '=', request.env.company.id),
|
||||
])
|
||||
lines = ["Geofence Data:"]
|
||||
for loc in locations:
|
||||
clock_ins = Log.search([
|
||||
('location_id', '=', loc.id),
|
||||
('log_type', '=', 'clock_in'),
|
||||
('log_date', '>=', datetime.combine(date_from, datetime.min.time())),
|
||||
('log_date', '<=', datetime.combine(date_to, datetime.max.time())),
|
||||
])
|
||||
outside = Log.search([
|
||||
('location_id', '=', loc.id),
|
||||
('log_type', '=', 'outside_geofence'),
|
||||
('log_date', '>=', datetime.combine(date_from, datetime.min.time())),
|
||||
('log_date', '<=', datetime.combine(date_to, datetime.max.time())),
|
||||
])
|
||||
distances = [l.distance for l in clock_ins if l.distance]
|
||||
avg_dist = sum(distances) / len(distances) if distances else 0
|
||||
outside_distances = [l.distance for l in outside if l.distance]
|
||||
avg_overshoot = sum(outside_distances) / len(outside_distances) if outside_distances else 0
|
||||
|
||||
lines.append(
|
||||
f"\n{loc.name} (radius: {loc.radius}m):\n"
|
||||
f" Successful clock-ins: {len(clock_ins)}, avg distance: {avg_dist:.0f}m\n"
|
||||
f" Blocked (outside): {len(outside)}, avg overshoot: {avg_overshoot:.0f}m"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@http.route('/fusion_clock_ai/smart_config', type='json', auth='user')
|
||||
def smart_config(self, description):
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
return {'error': 'Access denied.'}
|
||||
|
||||
AI = request.env['fusion.clock.ai.service'].sudo()
|
||||
system_prompt = AI._get_system_prompt('smart_config')
|
||||
|
||||
try:
|
||||
response = AI.chat_completion(
|
||||
[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': description},
|
||||
],
|
||||
feature='smart_config',
|
||||
)
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
return {'response': response}
|
||||
|
||||
@http.route('/fusion_clock_ai/apply_config', type='json', auth='user')
|
||||
def apply_config(self, changes):
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
return {'error': 'Access denied.'}
|
||||
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
applied = []
|
||||
for change in changes:
|
||||
param = change.get('parameter', '')
|
||||
value = change.get('value', '')
|
||||
if param.startswith('fusion_clock.'):
|
||||
ICP.set_param(param, str(value))
|
||||
applied.append(param)
|
||||
|
||||
return {'applied': applied, 'count': len(applied)}
|
||||
121
fusion_clock_ai/controllers/portal_ai.py
Normal file
121
fusion_clock_ai/controllers/portal_ai.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, date
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionClockPortalAI(http.Controller):
|
||||
|
||||
def _get_employee(self):
|
||||
return request.env['hr.employee'].sudo().search([
|
||||
('user_id', '=', request.env.uid),
|
||||
], limit=1)
|
||||
|
||||
@http.route('/fusion_clock_ai/employee_chat', type='json', auth='user')
|
||||
def employee_chat(self, message, conversation_id=None):
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
if ICP.get_param('fusion_clock_ai.enable_employee_chat', 'True') != 'True':
|
||||
return {'error': 'AI Assistant is currently disabled.'}
|
||||
|
||||
employee = self._get_employee()
|
||||
if not employee:
|
||||
return {'error': 'No employee record found.'}
|
||||
|
||||
AI = request.env['fusion.clock.ai.service'].sudo()
|
||||
|
||||
context_data = AI._build_employee_context(
|
||||
employee,
|
||||
date.today() - timedelta(days=30),
|
||||
date.today(),
|
||||
)
|
||||
|
||||
Conversation = request.env['fusion.clock.ai.conversation'].sudo()
|
||||
if conversation_id:
|
||||
conv = Conversation.browse(int(conversation_id))
|
||||
if not conv.exists() or conv.user_id.id != request.env.uid:
|
||||
conv = Conversation.create({
|
||||
'user_id': request.env.uid,
|
||||
'conversation_type': 'employee_chat',
|
||||
})
|
||||
else:
|
||||
conv = Conversation.create({
|
||||
'user_id': request.env.uid,
|
||||
'conversation_type': 'employee_chat',
|
||||
})
|
||||
|
||||
Message = request.env['fusion.clock.ai.message'].sudo()
|
||||
Message.create({
|
||||
'conversation_id': conv.id,
|
||||
'role': 'user',
|
||||
'content': message,
|
||||
})
|
||||
|
||||
system_prompt = AI._get_system_prompt('employee_chat')
|
||||
messages = [
|
||||
{'role': 'system', 'content': f"{system_prompt}\n\n--- YOUR ATTENDANCE DATA ---\n{context_data}"},
|
||||
]
|
||||
for msg in conv.message_ids:
|
||||
messages.append({'role': msg.role, 'content': msg.content})
|
||||
|
||||
try:
|
||||
response = AI.chat_completion(messages, feature='employee_chat')
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
Message.create({
|
||||
'conversation_id': conv.id,
|
||||
'role': 'assistant',
|
||||
'content': response,
|
||||
})
|
||||
|
||||
return {
|
||||
'response': response,
|
||||
'conversation_id': conv.id,
|
||||
}
|
||||
|
||||
@http.route('/fusion_clock_ai/polish_reason', type='json', auth='user')
|
||||
def polish_reason(self, rough_text):
|
||||
AI = request.env['fusion.clock.ai.service'].sudo()
|
||||
system_prompt = AI._get_system_prompt('leave_reason_writer')
|
||||
try:
|
||||
response = AI.chat_completion(
|
||||
[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': rough_text},
|
||||
],
|
||||
feature='leave_reason_writer',
|
||||
)
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
return {'polished': response}
|
||||
|
||||
@http.route('/fusion_clock_ai/my_coach_tip', type='json', auth='user')
|
||||
def my_coach_tip(self):
|
||||
employee = self._get_employee()
|
||||
if not employee:
|
||||
return {'error': 'No employee record found.'}
|
||||
|
||||
AI = request.env['fusion.clock.ai.service'].sudo()
|
||||
context_data = AI._build_employee_context(
|
||||
employee,
|
||||
date.today() - timedelta(days=14),
|
||||
date.today(),
|
||||
)
|
||||
system_prompt = AI._get_system_prompt('attendance_coach')
|
||||
try:
|
||||
response = AI.chat_completion(
|
||||
[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': context_data},
|
||||
],
|
||||
feature='attendance_coach',
|
||||
)
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
return {'tip': response}
|
||||
Reference in New Issue
Block a user