# -*- coding: utf-8 -*- import random import sys import uuid import httpx from odoo import models, fields from . import decode_template class AlphaIdea(models.Model): _name = 'alpha.idea' _description = 'Alpha Idea' _order = 'id desc' name = fields.Char(string='Name', required=True, default=lambda self: str(uuid.uuid4())) status = fields.Selection([ ('draft', 'Draft'), ('fetched_data', 'Fetched Data'), ('generated_prompt', 'Generated Prompt'), ('posted_to_ms', 'Posted To MS'), ('llm_received', 'LLM Received'), ('decode_template', 'Decode Template'), ('decoded', 'Decoded'), ('done', 'Done'), ('failed', 'Failed'), ('cancel', 'Cancel') ], string='Status', default='draft') pushed = fields.Boolean(string='Pushed', default=False, readonly=True) region = fields.Many2one('alpha.region.settings', string='Region', required=True) universe = fields.Many2one('alpha.universe.settings', string='Universe', required=True) data_type = fields.Selection([('MATRIX', 'MATRIX'), ('VECTOR', 'VECTOR')], string='Data Type', required=True, default='MATRIX') delay = fields.Selection([('1', '1'), ('0', '0')], required=True, string='Delay', default='1') data_sets = fields.Char(string='Data Sets', compute='_compute_data_sets') meta_prompt = fields.Many2one('alpha.prompt.settings', string='Meta Prompt') replace_prompt = fields.Text(string='Replace Prompt') final_prompt = fields.Text(string='Final Prompt') system_prompt = fields.Text(string='System Prompt') user_prompt = fields.Text(string='User Prompt') llm_generated_idea = fields.Text(string='LLM Generated Idea') result_message = fields.Text(string='Result Message') llm_settings_line_id = fields.Many2one('llm.settings.line', string='Model', default=lambda self: self._default_llm_settings_line_id()) def _default_llm_settings_line_id(self): """默认随机选择一个模型,如果没有则返回 False""" llm_lines = self.env['llm.settings.line'].search([]) if llm_lines: return random.choice(llm_lines).id return False idea_template_ids = fields.One2many('alpha.idea.template', 'idea_id', string='Idea Templates') needed_data_set_ids = fields.One2many('alpha.needed.data.set', 'idea_id', string='Needed Data Sets') final_expression_ids = fields.One2many('alpha.final.expression', 'idea_id', string='Final Expressions') expression_count = fields.Integer(string='Expression Count', required=True, readonly=True, default=0, compute='_compute_expression_count') def _compute_data_sets(self): # 显示使用的数据集 for record in self: record.data_sets = '' if record.needed_data_set_ids: data_sets_list = [] for data_set_name in record.needed_data_set_ids: data_sets_list.append(data_set_name.name) if len(data_sets_list) > 0: record.data_sets = ', '.join(data_sets_list) def action_cancel(self): self.status = 'cancel' def action_reset(self): self.status = 'fetched_data' def btn_check_and_fetch_data(self): if not self.needed_data_set_ids: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No data', 'message': 'No data set is needed.', 'type': 'danger', 'sticky': False, } } # TODO: 通过 datasets_id, 查找 datasets 中, 是否存在 datasets_id 的数据, 如果不存在, 则在 datasets 模块中创建一条记录, 然后需要过去手动下载(必须) success = False for dataset in self.needed_data_set_ids: dataset_id = self.env['alpha.datasets'].search( [('datasets_id', '=', dataset.name), ('region', '=', self.region.id), ('universe', '=', self.universe.id), ('delay', '=', self.delay) ], limit=1) if not dataset_id: dataset_id = self.env['alpha.datasets'].create({ 'name': str(uuid.uuid4()), 'datasets_id': dataset.name, 'region': self.region.id, 'universe': self.universe.id, 'delay': self.delay, }) # 创建 dataset_id 的记录之后, 执行一下 dataset_id 的 btn_get_datasets 方法 try: dataset_id.btn_get_datasets() success = True except Exception as e: self.status = 'failed' return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Failed', 'message': f'Dataset fetch failed: {str(e)}', 'type': 'danger', 'sticky': False, } } else: if dataset_id.line_ids: success = True else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No data', 'message': 'Dataset does not exist.', 'type': 'danger', 'sticky': False, } } if success: self.status = 'fetched_data' return True def btn_generate_final_prompt(self): if self.final_prompt: self.final_prompt = '' self.system_prompt = '' self.user_prompt = '' self.status = 'fetched_data' return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': '成功', 'message': '已清除最终提示词', 'type': 'success', 'sticky': False, } } if not self.meta_prompt: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No data!', 'message': 'Please select the prompt template first.', 'type': 'danger', 'sticky': False, } } if not self.replace_prompt: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No data!', 'message': 'Research direction cannot be empty.', 'type': 'danger', 'sticky': False, } } system_prompt = '''You are executing two skills in sequence: 1) brain-data-feature-engineering 2) brain-feature-implementation The following SKILL.md documents are authoritative; follow them exactly. ''' data_sets_list = [] dataset_id = '' category = '' region = self.region.name delay = self.delay universe = self.universe.name for dataset in self.needed_data_set_ids: datasets_id = self.env['alpha.datasets'].search( [('datasets_id', '=', dataset.name)], limit=1) for datasets_line in datasets_id.line_ids: data_sets_list.append({ 'id': datasets_line.data_field_name, 'description': datasets_line.description }) if not dataset_id: dataset_id = datasets_line.dataset_id if not category: category = datasets_line.category_name if not data_sets_list: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No data!', 'message': 'No related dataset found.', 'type': 'danger', 'sticky': False, } } field_count = len(data_sets_list) operator_ids = self.env['alpha.operator.line'].search([]) operator_list = [] for operator in operator_ids: operator_list.append({ 'name': operator.name, 'category': operator.category, 'scope': operator.scope, 'description': operator.description, 'definition': operator.definition }) if not operator_list: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No data!', 'message': 'No related Operator found.', 'type': 'danger', 'sticky': False, } } fields_json = ','.join([str(field) for field in data_sets_list]) user_prompt = '''{ "instructions": { "output_format": "Fill OUTPUT_TEMPLATE.md with concrete content.", "implementation_examples": "Each Implementation Example must be a template with {variable} placeholders. Use only placeholders from allowed_placeholders. Use suffix-only names; do not include dataset code/prefix/horizon.", "no_code_fences": true, "do_not_invent_placeholders": true }, "dataset_context": { "dataset_id": "''' + dataset_id + '''", "dataset_name": null, "dataset_description": null, "category": "''' + category + '''", "region": "''' + region + '''", "delay": ''' + str(delay) + ''', "universe": "''' + universe + '''", "field_count": ''' + str(field_count) + ''' }, "fields": [ ''' + fields_json + ''' ] } ''' final_prompt = self.meta_prompt.prompt final_prompt = final_prompt.replace( '###question_driven###', self.replace_prompt) final_prompt = final_prompt.replace('###dataset_id###', dataset_id) final_prompt = final_prompt.replace('###category###', category) final_prompt = final_prompt.replace('###region###', region) final_prompt = final_prompt.replace('###delay###', str(delay)) final_prompt = final_prompt.replace( '###field_count###', str(field_count)) final_prompt = final_prompt.replace('###universe###', universe) final_prompt = final_prompt.replace( '###datasets###', str(data_sets_list)) final_prompt = final_prompt.replace( '###operators###', str(operator_list)) if self.data_type and self.data_type.upper() == "VECTOR": vector_prompt = "since all the following the data is vector type data, before you do any process, you should choose a vector operator to generate its statistical feature to use, the data cannot be directly use. for example, if datafieldA and datafieldB are vector type data, you can use vec_avg(datafieldA) - vec_avg(datafieldB), where vec_avg() operator is used to generate the average of the data on a certain date. similarly, vector type operator can only be used on the vector type operator directly and cannot be nested, for example vec_avg(vec_sum(datafield)) is a false use." final_prompt = final_prompt.replace( '###vector_instruction###', vector_prompt) else: final_prompt = final_prompt.replace('###vector_instruction###', '') self.system_prompt = system_prompt self.user_prompt = user_prompt self.final_prompt = final_prompt self.status = 'generated_prompt' return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': '成功', 'message': f'已生成提示模板,包含 {field_count} 个数据字段', 'type': 'success', 'sticky': False, } } def post_to_ms(self): if not all([self.final_prompt, self.user_prompt, self.system_prompt]): return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Error', 'message': 'Please generate a prompt template.', 'type': 'danger', 'sticky': False, } } if not self.llm_settings_line_id: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Error', 'message': 'Please select a model.', 'type': 'danger', 'sticky': False, } } model_name = self.llm_settings_line_id.model_name base_url = self.llm_settings_line_id.llm_setting_id.base_url api_key = self.llm_settings_line_id.llm_setting_id.api_key # 获取当前 record_id record_id = self.id # 获取 Odoo 回调地址 base_url_odoo = self.env['ir.config_parameter'].sudo().get_param('web.base.url') callback_url = f"{base_url_odoo}/api/alpha-idea/result" # 组装完整 prompt full_prompt = f"{self.system_prompt}\n\n{self.user_prompt}\n\n{self.final_prompt}" # 获取微服务配置 ms_config = self.get_ms_config() ms_url = ms_config.get('url', '') if not ms_url: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Error', 'message': 'Microservice address not configured.', 'type': 'danger', 'sticky': False } } # 组装请求数据 payload = { 'record_id': record_id, 'prompt': full_prompt, 'model_name': model_name, 'base_url': base_url, 'api_key': api_key, 'callback_url': callback_url, 'system_prompt': self.system_prompt, 'user_prompt': self.user_prompt, 'final_prompt': self.final_prompt, } # 发送请求到微服务 try: httpx.post(f"{ms_url}:32004/api_alpha_generate_idea", json=payload, timeout=0.001) except httpx.TimeoutException: pass except Exception as e: self.status = 'failed' return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Error', 'message': f'Failed to send microservice:\n{e}.', 'type': 'danger', 'sticky': False, } } self.status = 'posted_to_ms' def get_ms_config(self): # TODO 先从 nacos 获取微服务 url nacos_url = '' platform_info = sys.platform if platform_info == "darwin": nacos_url = 'http://192.168.31.41:30848/nacos/v1/cs/configs?dataId=microservices_dev&group=quantify' elif platform_info.startswith("linux"): nacos_url = 'http://192.168.31.41:30848/nacos/v1/cs/configs?dataId=microservices&group=quantify' try: ms_config_resp = httpx.get(nacos_url) ms_config_resp.raise_for_status() except Exception as e: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Error', 'message': f'Nacos request failed:\n{e}.', 'type': 'danger', 'sticky': False, } } ms_config = ms_config_resp.json() return ms_config def action_set_llm_received(self): """手动设置 LLM 已返回状态 当手动录入 llm_generated_idea 后,调用此函数将状态改为 llm_received """ if self.llm_generated_idea and self.status == 'generated_prompt': self.status = 'llm_received' return True else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Error', 'message': 'llm_generated_idea 无数据或状态不是 Generated Prompt', 'type': 'danger', 'sticky': False, } } def decode_template(self): if not self.llm_generated_idea or self.status != 'llm_received': return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Please use llm to generate the template first.', 'message': 'llm_generated_idea no data or status != llm_received', 'type': 'danger', 'sticky': False, } } if self.status == 'done': return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'message', 'message': 'This idea has been generated, please do not repeat the operation.', 'type': 'danger', 'sticky': False, } } llm_template = self.llm_generated_idea data_sets_list = [] for dataset in self.needed_data_set_ids: datasets_id = self.env['alpha.datasets'].search( [('datasets_id', '=', dataset.name)], limit=1) for datasets_line in datasets_id.line_ids: data_sets_list.append({ 'id': datasets_line.data_field_name }) result_data = decode_template.process(data_sets_list, llm_template) if result_data['success']: templates = result_data['templates'] expressions = result_data['expressions'] # 如果没有解出数据,不做任何操作 if len(templates) == 0 and len(expressions) == 0: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': '提示', 'message': '未解码出任何数据', 'type': 'warning', 'sticky': False, } } # 组装模板数据 template_data = [] for template_item in templates: template_data.append({ 'template': template_item['template'], 'original_template': template_item['original_template'], 'idea': template_item.get('idea', ''), 'template_line_ids': [(0, 0, {'expression': line}) for line in template_item['expressions']] }) # 保存数据 self.write({ 'idea_template_ids': [(0, 0, data) for data in template_data], 'final_expression_ids': [(0, 0, {'name': expression}) for expression in expressions], 'status': 'decoded', }) return True else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': '失败', 'message': '模板解码失败, 生成数量为 0', 'type': 'danger', 'sticky': False, } } def combination_settings(self): self.ensure_one() if self.idea_template_ids and self.final_expression_ids: return { 'type': 'ir.actions.act_window', 'name': '组合设置', 'res_model': 'wizard.combination.settings', 'view_mode': 'form', 'target': 'new', 'context': { 'active_model': 'alpha.idea', 'active_id': self.id, }, } else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Execute the decoding template first', 'message': 'idea_template_ids and final_expression_ids no data!', 'type': 'danger', 'sticky': False, } } def _compute_expression_count(self): for record in self: if record.final_expression_ids: record.expression_count = len(record.final_expression_ids) else: record.expression_count = 0 class NeededDataSet(models.Model): _name = 'alpha.needed.data.set' _description = 'Alpha Needed Data Set' idea_id = fields.Many2one('alpha.idea', string='Idea', ondelete='cascade') name = fields.Char(string='Name') class IdeaTemplate(models.Model): _name = 'alpha.idea.template' _description = 'Alpha Idea Template' idea_id = fields.Many2one('alpha.idea', string='Idea', ondelete='cascade') template_line_ids = fields.One2many( 'alpha.idea.template.line', 'idea_id', string='Template Lines') template = fields.Char(string='Template') original_template = fields.Char(string='Original Template') expression_count = fields.Integer( string='Expression Count', compute='_compute_expression_count', default=0) idea = fields.Text(string='Text') def _compute_expression_count(self): for record in self: record.expression_count = len(record.template_line_ids) class IdeaTemplateLine(models.Model): _name = 'alpha.idea.template.line' _description = 'Alpha Idea Template Line' idea_id = fields.Many2one('alpha.idea.template', string='Alpha Idea Template', ondelete='cascade') expression = fields.Char(string='Expression') class Final_Expression(models.Model): _name = 'alpha.final.expression' _description = 'Alpha Final Expression' idea_id = fields.Many2one('alpha.idea', string='Idea', ondelete='cascade') name = fields.Char(string='Name')