diff --git a/common/models/interaction.py b/common/models/interaction.py index 4e656af..24024b4 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -67,25 +67,23 @@ class EveAIAsset(db.Model): description = db.Column(db.Text, nullable=True) type = db.Column(db.String(50), nullable=False, default="DOCUMENT_TEMPLATE") type_version = db.Column(db.String(20), nullable=True, default="1.0.0") - valid_from = db.Column(db.DateTime, nullable=True) - valid_to = db.Column(db.DateTime, nullable=True) - # Versioning Information - created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) - created_by = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) - updated_by = db.Column(db.Integer, db.ForeignKey(User.id)) - - # Relations - versions = db.relationship('EveAIAssetVersion', backref='asset', lazy=True) - - -class EveAIAssetVersion(db.Model): - id = db.Column(db.Integer, primary_key=True) - asset_id = db.Column(db.Integer, db.ForeignKey(EveAIAsset.id), nullable=False) + # Storage information bucket_name = db.Column(db.String(255), nullable=True) + object_name = db.Column(db.String(200), nullable=True) + file_type = db.Column(db.String(20), nullable=True) + file_size = db.Column(db.Float, nullable=True) + + # Metadata information + user_metadata = db.Column(JSONB, nullable=True) + system_metadata = db.Column(JSONB, nullable=True) + + # Configuration information configuration = db.Column(JSONB, nullable=True) - arguments = db.Column(JSONB, nullable=True) + + # Cost information + prompt_tokens = db.Column(db.Integer, nullable=True) + completion_tokens = db.Column(db.Integer, nullable=True) # Versioning Information created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) @@ -93,25 +91,7 @@ class EveAIAssetVersion(db.Model): updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) updated_by = db.Column(db.Integer, db.ForeignKey(User.id)) - # Relations - instructions = db.relationship('EveAIAssetInstruction', backref='asset_version', lazy=True) - - -class EveAIAssetInstruction(db.Model): - id = db.Column(db.Integer, primary_key=True) - asset_version_id = db.Column(db.Integer, db.ForeignKey(EveAIAssetVersion.id), nullable=False) - name = db.Column(db.String(255), nullable=False) - content = db.Column(db.Text, nullable=True) - - -class EveAIProcessedAsset(db.Model): - id = db.Column(db.Integer, primary_key=True) - asset_version_id = db.Column(db.Integer, db.ForeignKey(EveAIAssetVersion.id), nullable=False) - specialist_id = db.Column(db.Integer, db.ForeignKey(Specialist.id), nullable=True) - chat_session_id = db.Column(db.Integer, db.ForeignKey(ChatSession.id), nullable=True) - bucket_name = db.Column(db.String(255), nullable=True) - object_name = db.Column(db.String(255), nullable=True) - created_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now()) + last_used_at = db.Column(db.DateTime, nullable=True) class EveAIAgent(db.Model): diff --git a/common/services/interaction/asset_services.py b/common/services/interaction/asset_services.py new file mode 100644 index 0000000..8083257 --- /dev/null +++ b/common/services/interaction/asset_services.py @@ -0,0 +1,9 @@ +from common.models.interaction import EveAIAsset +from common.extensions import minio_client + + +class AssetServices: + + @staticmethod + def add_or_replace_asset_file(asset_id, file_data): + asset = EveAIAsset.query.get_or_404(asset_id) diff --git a/common/services/utils/answer_check_services.py b/common/services/utils/answer_check_services.py new file mode 100644 index 0000000..9363601 --- /dev/null +++ b/common/services/utils/answer_check_services.py @@ -0,0 +1,65 @@ +from flask import current_app, session +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnablePassthrough + +from common.utils.business_event import BusinessEvent +from common.utils.business_event_context import current_event +from common.utils.model_utils import get_template +from eveai_chat_workers.outputs.globals.q_a_output.q_a_output_v1_0 import QAOutput + + +class AnswerCheckServices: + @staticmethod + def check_affirmative_answer(question: str, answer: str, language_iso: str) -> bool: + return AnswerCheckServices._check_answer(question, answer, language_iso, "check_affirmative_answer", + "Check Affirmative Answer") + + @staticmethod + def check_additional_information(question: str, answer: str, language_iso: str) -> bool: + return AnswerCheckServices._check_answer(question, answer, language_iso, "check_additional_information", + "Check Additional Information") + + @staticmethod + def _check_answer(question: str, answer: str, language_iso: str, template_name: str, span_name: str) -> bool: + if language_iso.strip() == '': + raise ValueError("Language cannot be empty") + language = current_app.config.get('SUPPORTED_LANGUAGE_ISO639_1_LOOKUP').get(language_iso) + if language is None: + raise ValueError(f"Unsupported language: {language_iso}") + if question.strip() == '': + raise ValueError("Question cannot be empty") + if answer.strip() == '': + raise ValueError("Answer cannot be empty") + + tenant_id = session.get('tenant').get('id') + + if not current_event: + with BusinessEvent('Answer Check Service', tenant_id): + with current_event.create_span(span_name): + return AnswerCheckServices._check_answer_logic(question, answer, language, template_name) + else: + with current_event.create_span('Check Affirmative Answer'): + return AnswerCheckServices._check_answer_logic(question, answer, language, template_name) + + @staticmethod + def _check_answer_logic(question: str, answer: str, language: str, template_name: str) -> bool: + prompt_params = { + 'question': question, + 'answer': answer, + 'language': language, + } + + template, llm = get_template(template_name) + check_answer_prompt = ChatPromptTemplate.from_template(template) + setup = RunnablePassthrough() + + output_schema = QAOutput + structured_llm = llm.with_structured_output(output_schema) + + chain = (setup | check_answer_prompt | structured_llm ) + + raw_answer = chain.invoke(prompt_params) + current_app.logger.debug(f"Raw answer: {raw_answer}") + + return raw_answer.answer diff --git a/common/services/utils/translation_services.py b/common/services/utils/translation_services.py index 0ef3186..cb551a9 100644 --- a/common/services/utils/translation_services.py +++ b/common/services/utils/translation_services.py @@ -1,5 +1,8 @@ import json from typing import Dict, Any, Optional + +from flask import session + from common.extensions import cache_manager from common.utils.business_event import BusinessEvent from common.utils.business_event_context import current_event @@ -7,11 +10,13 @@ from common.utils.business_event_context import current_event class TranslationServices: @staticmethod - def translate_config(config_data: Dict[str, Any], field_config: str, target_language: str, source_language: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]: + def translate_config(tenant_id: int, config_data: Dict[str, Any], field_config: str, target_language: str, + source_language: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]: """ Vertaalt een configuratie op basis van een veld-configuratie. Args: + tenant_id: Identificatie van de tenant waarvoor we de vertaling doen. config_data: Een dictionary of JSON (die dan wordt geconverteerd naar een dictionary) met configuratiegegevens field_config: De naam van een veld-configuratie (bijv. 'fields') target_language: De taal waarnaar vertaald moet worden @@ -21,6 +26,26 @@ class TranslationServices: Returns: Een dictionary met de vertaalde configuratie """ + config_type = config_data.get('type', 'Unknown') + config_version = config_data.get('version', 'Unknown') + span_name = f"{config_type}-{config_version}-{field_config}" + + if current_event: + with current_event.create_span(span_name): + translated_config = TranslationServices._translate_config(tenant_id, config_data, field_config, + target_language, source_language, context) + return translated_config + else: + with BusinessEvent('Config Translation Service', tenant_id): + with current_event.create_span(span_name): + translated_config = TranslationServices._translate_config(tenant_id, config_data, field_config, + target_language, source_language, context) + return translated_config + + @staticmethod + def _translate_config(tenant_id: int, config_data: Dict[str, Any], field_config: str, target_language: str, + source_language: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]: + # Zorg ervoor dat we een dictionary hebben if isinstance(config_data, str): config_data = json.loads(config_data) @@ -31,78 +56,79 @@ class TranslationServices: # Haal type en versie op voor de Business Event span config_type = config_data.get('type', 'Unknown') config_version = config_data.get('version', 'Unknown') - span_name = f"{config_type}-{config_version}-{field_config}" - # Start een Business Event context - with BusinessEvent('Config Translation Service', 0): - with current_event.create_span(span_name): - # Controleer of de gevraagde veld-configuratie bestaat - if field_config in config_data: - fields = config_data[field_config] + if field_config in config_data: + fields = config_data[field_config] - # Haal description uit metadata voor context als geen context is opgegeven - description_context = "" - if not context and 'metadata' in config_data and 'description' in config_data['metadata']: - description_context = config_data['metadata']['description'] + # Haal description uit metadata voor context als geen context is opgegeven + description_context = "" + if not context and 'metadata' in config_data and 'description' in config_data['metadata']: + description_context = config_data['metadata']['description'] - # Loop door elk veld in de configuratie - for field_name, field_data in fields.items(): - # Vertaal name als het bestaat en niet leeg is - if 'name' in field_data and field_data['name']: - # Gebruik context indien opgegeven, anders description_context - field_context = context if context else description_context - translated_name = cache_manager.translation_cache.get_translation( - text=field_data['name'], - target_lang=target_language, - source_lang=source_language, - context=field_context - ) - if translated_name: - translated_config[field_config][field_name]['name'] = translated_name.translated_text + # Loop door elk veld in de configuratie + for field_name, field_data in fields.items(): + # Vertaal name als het bestaat en niet leeg is + if 'name' in field_data and field_data['name']: + # Gebruik context indien opgegeven, anders description_context + field_context = context if context else description_context + translated_name = cache_manager.translation_cache.get_translation( + text=field_data['name'], + target_lang=target_language, + source_lang=source_language, + context=field_context + ) + if translated_name: + translated_config[field_config][field_name]['name'] = translated_name.translated_text - if 'title' in field_data and field_data['title']: - # Gebruik context indien opgegeven, anders description_context - field_context = context if context else description_context - translated_title = cache_manager.translation_cache.get_translation( - text=field_data['title'], - target_lang=target_language, - source_lang=source_language, - context=field_context - ) - if translated_title: - translated_config[field_config][field_name]['title'] = translated_title.translated_text + if 'title' in field_data and field_data['title']: + # Gebruik context indien opgegeven, anders description_context + field_context = context if context else description_context + translated_title = cache_manager.translation_cache.get_translation( + text=field_data['title'], + target_lang=target_language, + source_lang=source_language, + context=field_context + ) + if translated_title: + translated_config[field_config][field_name]['title'] = translated_title.translated_text - # Vertaal description als het bestaat en niet leeg is - if 'description' in field_data and field_data['description']: - # Gebruik context indien opgegeven, anders description_context - field_context = context if context else description_context - translated_desc = cache_manager.translation_cache.get_translation( - text=field_data['description'], - target_lang=target_language, - source_lang=source_language, - context=field_context - ) - if translated_desc: - translated_config[field_config][field_name]['description'] = translated_desc.translated_text + # Vertaal description als het bestaat en niet leeg is + if 'description' in field_data and field_data['description']: + # Gebruik context indien opgegeven, anders description_context + field_context = context if context else description_context + translated_desc = cache_manager.translation_cache.get_translation( + text=field_data['description'], + target_lang=target_language, + source_lang=source_language, + context=field_context + ) + if translated_desc: + translated_config[field_config][field_name]['description'] = translated_desc.translated_text - # Vertaal context als het bestaat en niet leeg is - if 'context' in field_data and field_data['context']: - translated_ctx = cache_manager.translation_cache.get_translation( - text=field_data['context'], - target_lang=target_language, - source_lang=source_language, - context=context - ) - if translated_ctx: - translated_config[field_config][field_name]['context'] = translated_ctx.translated_text + # Vertaal context als het bestaat en niet leeg is + if 'context' in field_data and field_data['context']: + translated_ctx = cache_manager.translation_cache.get_translation( + text=field_data['context'], + target_lang=target_language, + source_lang=source_language, + context=context + ) + if translated_ctx: + translated_config[field_config][field_name]['context'] = translated_ctx.translated_text - return translated_config + return translated_config @staticmethod - def translate(text: str, target_language: str, source_language: Optional[str] = None, + def translate(tenant_id: int, text: str, target_language: str, source_language: Optional[str] = None, context: Optional[str] = None)-> str: - with BusinessEvent('Translation Service', 0): + if current_event: with current_event.create_span('Translation'): translation_cache = cache_manager.translation_cache.get_translation(text, target_language, source_language, context) - return translation_cache.translated_text \ No newline at end of file + return translation_cache.translated_text + else: + with BusinessEvent('Translation Service', tenant_id): + with current_event.create_span('Translation'): + translation_cache = cache_manager.translation_cache.get_translation(text, target_language, + source_language, context) + return translation_cache.translated_text \ No newline at end of file diff --git a/common/utils/asset_utils.py b/common/utils/asset_utils.py index 9b600bd..9695a41 100644 --- a/common/utils/asset_utils.py +++ b/common/utils/asset_utils.py @@ -4,59 +4,9 @@ from flask import current_app from sqlalchemy.exc import SQLAlchemyError from common.extensions import cache_manager, minio_client, db -from common.models.interaction import EveAIAsset, EveAIAssetVersion +from common.models.interaction import EveAIAsset from common.utils.model_logging_utils import set_logging_information -def create_asset_stack(api_input, tenant_id): - type_version = cache_manager.assets_version_tree_cache.get_latest_version(api_input['type']) - api_input['type_version'] = type_version - new_asset = create_asset(api_input, tenant_id) - new_asset_version = create_version_for_asset(new_asset, tenant_id) - db.session.add(new_asset) - db.session.add(new_asset_version) - - try: - db.session.commit() - except SQLAlchemyError as e: - current_app.logger.error(f"Could not add asset for tenant {tenant_id}: {str(e)}") - db.session.rollback() - raise e - - return new_asset, new_asset_version - - -def create_asset(api_input, tenant_id): - new_asset = EveAIAsset() - new_asset.name = api_input['name'] - new_asset.description = api_input['description'] - new_asset.type = api_input['type'] - new_asset.type_version = api_input['type_version'] - if api_input['valid_from'] and api_input['valid_from'] != '': - new_asset.valid_from = api_input['valid_from'] - else: - new_asset.valid_from = dt.now(tz.utc) - new_asset.valid_to = api_input['valid_to'] - set_logging_information(new_asset, dt.now(tz.utc)) - - return new_asset - - -def create_version_for_asset(asset, tenant_id): - new_asset_version = EveAIAssetVersion() - new_asset_version.asset = asset - new_asset_version.bucket_name = minio_client.create_tenant_bucket(tenant_id) - set_logging_information(new_asset_version, dt.now(tz.utc)) - - return new_asset_version - - -def add_asset_version_file(asset_version, field_name, file, tenant_id): - object_name, file_size = minio_client.upload_file(asset_version.bucket_name, asset_version.id, field_name, - file.content_type) - # mark_tenant_storage_dirty(tenant_id) - # TODO - zorg ervoor dat de herberekening van storage onmiddellijk gebeurt! - return object_name - diff --git a/common/utils/minio_utils.py b/common/utils/minio_utils.py index 9cf372c..78ca4d1 100644 --- a/common/utils/minio_utils.py +++ b/common/utils/minio_utils.py @@ -33,8 +33,8 @@ class MinioClient: def generate_object_name(self, document_id, language, version_id, filename): return f"{document_id}/{language}/{version_id}/{filename}" - def generate_asset_name(self, asset_version_id, file_name, content_type): - return f"assets/{asset_version_id}/{file_name}.{content_type}" + def generate_asset_name(self, asset_id, asset_type, content_type): + return f"assets/{asset_type}/{asset_id}.{content_type}" def upload_document_file(self, tenant_id, document_id, language, version_id, filename, file_data): bucket_name = self.generate_bucket_name(tenant_id) @@ -57,8 +57,10 @@ class MinioClient: except S3Error as err: raise Exception(f"Error occurred while uploading file: {err}") - def upload_asset_file(self, bucket_name, asset_version_id, file_name, file_type, file_data): - object_name = self.generate_asset_name(asset_version_id, file_name, file_type) + def upload_asset_file(self, tenant_id: int, asset_id: int, asset_type: str, file_type: str, + file_data: bytes | FileStorage | io.BytesIO | str,) -> tuple[str, str, int]: + bucket_name = self.generate_bucket_name(tenant_id) + object_name = self.generate_asset_name(asset_id, asset_type, file_type) try: if isinstance(file_data, FileStorage): @@ -73,7 +75,7 @@ class MinioClient: self.client.put_object( bucket_name, object_name, io.BytesIO(file_data), len(file_data) ) - return object_name, len(file_data) + return bucket_name, object_name, len(file_data) except S3Error as err: raise Exception(f"Error occurred while uploading asset: {err}") @@ -84,6 +86,13 @@ class MinioClient: except S3Error as err: raise Exception(f"Error occurred while downloading file: {err}") + def download_asset_file(self, tenant_id, bucket_name, object_name): + try: + response = self.client.get_object(bucket_name, object_name) + return response.read() + except S3Error as err: + raise Exception(f"Error occurred while downloading asset: {err}") + def list_document_files(self, tenant_id, document_id, language=None, version_id=None): bucket_name = self.generate_bucket_name(tenant_id) prefix = f"{document_id}/" @@ -105,3 +114,9 @@ class MinioClient: return True except S3Error as err: raise Exception(f"Error occurred while deleting file: {err}") + + def delete_object(self, bucket_name, object_name): + try: + self.client.remove_object(bucket_name, object_name) + except S3Error as err: + raise Exception(f"Error occurred while deleting object: {err}") \ No newline at end of file diff --git a/config/assets/traicie/TRAICIE_KO_CRITERIA_QUESTIONS/1.0.0.yaml b/config/assets/traicie/TRAICIE_KO_CRITERIA_QUESTIONS/1.0.0.yaml new file mode 100644 index 0000000..e1dffad --- /dev/null +++ b/config/assets/traicie/TRAICIE_KO_CRITERIA_QUESTIONS/1.0.0.yaml @@ -0,0 +1,15 @@ +version: "1.0.0" +name: "Traicie KO Criteria Questions" +file_type: "yaml" +dynamic: true +configuration: + specialist_id: + name: "Specialist ID" + type: "int" + description: "The Specialist this asset is created for" + required: True +metadata: + author: "Josako" + date_added: "2025-07-01" + description: "Asset that defines a KO Criteria Questions and Answers" + changes: "Initial version" diff --git a/config/prompts/globals/check_additional_information/1.0.0.yaml b/config/prompts/globals/check_additional_information/1.0.0.yaml new file mode 100644 index 0000000..08bd4a4 --- /dev/null +++ b/config/prompts/globals/check_additional_information/1.0.0.yaml @@ -0,0 +1,14 @@ +version: "1.0.0" +content: > + Check if additional information or questions are available in the answer (answer in {language}), additional to the + following question: + + "{question}" + + Answer with True or False, without additional information. +llm_model: "mistral.mistral-medium-latest" +metadata: + author: "Josako" + date_added: "2025-06-23" + description: "An assistant to check if the answer to a question is affirmative." + changes: "Initial version" \ No newline at end of file diff --git a/config/prompts/globals/check_affirmative_answer/1.0.0.yaml b/config/prompts/globals/check_affirmative_answer/1.0.0.yaml new file mode 100644 index 0000000..88f61d4 --- /dev/null +++ b/config/prompts/globals/check_affirmative_answer/1.0.0.yaml @@ -0,0 +1,13 @@ +version: "1.0.0" +content: > + Determine if there is an affirmative answer on the following question in the provided answer (answer in {language}): + + {question} + + Answer with True or False, without additional information. +llm_model: "mistral.mistral-medium-latest" +metadata: + author: "Josako" + date_added: "2025-06-23" + description: "An assistant to check if the answer to a question is affirmative." + changes: "Initial version" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1.0.0.yaml b/config/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1.0.0.yaml new file mode 100644 index 0000000..7c1f935 --- /dev/null +++ b/config/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1.0.0.yaml @@ -0,0 +1,29 @@ +version: "1.1.0" +name: "Traicie KO Criteria Interview Definition Specialist" +framework: "crewai" +partner: "traicie" +chat: false +configuration: +arguments: + specialist_id: + name: "specialist_id" + description: "ID of the specialist for which to define KO Criteria Questions and Asnwers" + type: "integer" + required: true +results: + asset_id: + name: "asset_id" + description: "ID of the Asset containing questions and answers for each of the defined KO Criteria" + type: "integer" + required: true +agents: + - type: "TRAICIE_RECRUITER_AGENT" + version: "1.0" +tasks: + - type: "TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK" + version: "1.0" +metadata: + author: "Josako" + date_added: "2025-07-01" + changes: "Initial Version" + description: "Specialist assisting in questions and answers definition for KO Criteria" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml index 81822e7..8887dca 100644 --- a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml @@ -2,7 +2,7 @@ version: "1.0.0" name: "Traicie Selection Specialist" framework: "crewai" partner: "traicie" -chat: false +chat: true configuration: name: name: "Name" @@ -111,4 +111,4 @@ metadata: author: "Josako" date_added: "2025-05-27" changes: "Updated for unified competencies and ko criteria" - description: "Assistant to create a new Vacancy based on Vacancy Text" \ No newline at end of file + description: "Assistant to assist in candidate selection" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.1.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.1.0.yaml index 55389ff..789a1fa 100644 --- a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.1.0.yaml +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.1.0.yaml @@ -2,7 +2,7 @@ version: "1.1.0" name: "Traicie Selection Specialist" framework: "crewai" partner: "traicie" -chat: false +chat: true configuration: name: name: "Name" @@ -117,4 +117,4 @@ metadata: author: "Josako" date_added: "2025-05-27" changes: "Add make to the selection specialist" - description: "Assistant to create a new Vacancy based on Vacancy Text" \ No newline at end of file + description: "Assistant to assist in candidate selection" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml index 6988bb4..35f209c 100644 --- a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml @@ -2,7 +2,7 @@ version: "1.3.0" name: "Traicie Selection Specialist" framework: "crewai" partner: "traicie" -chat: false +chat: true configuration: name: name: "Name" @@ -117,4 +117,4 @@ metadata: author: "Josako" date_added: "2025-06-16" changes: "Realising the actual interaction with the LLM" - description: "Assistant to create a new Vacancy based on Vacancy Text" \ No newline at end of file + description: "Assistant to assist in candidate selection" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.3.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.3.0.yaml index 3f1dcbb..2bad44f 100644 --- a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.3.0.yaml +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.3.0.yaml @@ -2,7 +2,7 @@ version: "1.3.0" name: "Traicie Selection Specialist" framework: "crewai" partner: "traicie" -chat: false +chat: true configuration: name: name: "Name" @@ -117,4 +117,4 @@ metadata: author: "Josako" date_added: "2025-06-18" changes: "Add make to the selection specialist" - description: "Assistant to create a new Vacancy based on Vacancy Text" \ No newline at end of file + description: "Assistant to assist in candidate selection" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml new file mode 100644 index 0000000..3467c46 --- /dev/null +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml @@ -0,0 +1,124 @@ +version: "1.4.0" +name: "Traicie Selection Specialist" +framework: "crewai" +partner: "traicie" +chat: true +configuration: + name: + name: "Name" + description: "The name the specialist is called upon." + type: "str" + required: true + role_reference: + name: "Role Reference" + description: "A customer reference to the role" + type: "str" + required: false + make: + name: "Make" + description: "The make for which the role is defined and the selection specialist is created" + type: "system" + system_name: "tenant_make" + required: true + competencies: + name: "Competencies" + description: "An ordered list of competencies." + type: "ordered_list" + list_type: "competency_details" + required: true + tone_of_voice: + name: "Tone of Voice" + description: "The tone of voice the specialist uses to communicate" + type: "enum" + allowed_values: ["Professional & Neutral", "Warm & Empathetic", "Energetic & Enthusiastic", "Accessible & Informal", "Expert & Trustworthy", "No-nonsense & Goal-driven"] + default: "Professional & Neutral" + required: true + language_level: + name: "Language Level" + description: "Language level to be used when communicating, relating to CEFR levels" + type: "enum" + allowed_values: ["Basic", "Standard", "Professional"] + default: "Standard" + required: true + welcome_message: + name: "Welcome Message" + description: "Introductory text given by the specialist - but translated according to Tone of Voice, Language Level and Starting Language" + type: "text" + required: false + closing_message: + name: "Closing Message" + description: "Closing message given by the specialist - but translated according to Tone of Voice, Language Level and Starting Language" + type: "text" + required: false +competency_details: + title: + name: "Title" + description: "Competency Title" + type: "str" + required: true + description: + name: "Description" + description: "Description (in context of the role) of the competency" + type: "text" + required: true + is_knockout: + name: "KO" + description: "Defines if the competency is a knock-out criterium" + type: "boolean" + required: true + default: false + assess: + name: "Assess" + description: "Indication if this competency is to be assessed" + type: "boolean" + required: true + default: true +arguments: + region: + name: "Region" + type: "str" + description: "The region of the specific vacancy" + required: false + working_schedule: + name: "Work Schedule" + type: "str" + description: "The work schedule or employment type of the specific vacancy" + required: false + start_date: + name: "Start Date" + type: "date" + description: "The start date of the specific vacancy" + required: false + language: + name: "Language" + type: "str" + description: "The language (2-letter code) used to start the conversation" + required: true + interaction_mode: + name: "Interaction Mode" + type: "enum" + description: "The interaction mode the specialist will start working in." + allowed_values: ["Job Application", "Seduction"] + default: "Job Application" + required: true +results: + competencies: + name: "competencies" + type: "List[str, str]" + description: "List of vacancy competencies and their descriptions" + required: false +agents: + - type: "TRAICIE_RECRUITER_AGENT" + version: "1.0" + - type: "RAG_AGENT" + version: "1.0" +tasks: + - type: "TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK" + version: "1.0" + - type: "RAG_TASK" + version: "1.0" +metadata: + author: "Josako" + date_added: "2025-06-30" + changes: "Add 'RAG' to the selection specialist" + description: "Assistant to assist in candidate selection" \ No newline at end of file diff --git a/config/tasks/traicie/TRAICIE_COMPETENCIES_INTERVIEW_DEFINITION/1.0.0.yaml b/config/tasks/traicie/TRAICIE_COMPETENCIES_INTERVIEW_DEFINITION/1.0.0.yaml index d1867f3..f120e40 100644 --- a/config/tasks/traicie/TRAICIE_COMPETENCIES_INTERVIEW_DEFINITION/1.0.0.yaml +++ b/config/tasks/traicie/TRAICIE_COMPETENCIES_INTERVIEW_DEFINITION/1.0.0.yaml @@ -11,7 +11,7 @@ task_description: > Apply the following tone of voice in both questions and answers: {tone_of_voice} Apply the following language level in both questions and answers: {language_level} - Use {language} as language for both questions and answers. + Respect the language of the competencies, and return all output in the same language. ```{competencies}``` diff --git a/config/tasks/traicie/TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK/1.0.1.yaml b/config/tasks/traicie/TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK/1.0.1.yaml index e0dd724..f1c1aa7 100644 --- a/config/tasks/traicie/TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK/1.0.1.yaml +++ b/config/tasks/traicie/TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK/1.0.1.yaml @@ -17,7 +17,7 @@ task_description: > Apply the following language level in both questions and answers: {language_level}, i.e. {language_level_context} - Use {language} as language for both questions and answers. + Use the language used in the competencies as language for your answer / output. We call this the original language. ```{ko_criteria}``` @@ -26,9 +26,9 @@ task_description: > expected_output: > For each of the ko criteria, you provide: - the exact title as specified in the original language - - the question in {language} - - a positive answer, resulting in a positive evaluation of the criterium. In {language}. - - a negative answer, resulting in a negative evaluation of the criterium. In {language}. + - the question in the original language + - a positive answer, resulting in a positive evaluation of the criterium, in the original language. + - a negative answer, resulting in a negative evaluation of the criterium, in the original langauge. {custom_expected_output} metadata: author: "Josako" diff --git a/config/type_defs/asset_types.py b/config/type_defs/asset_types.py index 9408a27..927c745 100644 --- a/config/type_defs/asset_types.py +++ b/config/type_defs/asset_types.py @@ -1,5 +1,5 @@ # Agent Types -AGENT_TYPES = { +ASSET_TYPES = { "DOCUMENT_TEMPLATE": { "name": "Document Template", "description": "Asset that defines a template in markdown a specialist can process", @@ -8,4 +8,9 @@ AGENT_TYPES = { "name": "Specialist Configuration", "description": "Asset that defines a specialist configuration", }, + "TRAICIE_KO_CRITERIA_QUESTIONS": { + "name": "Traicie KO Criteria Questions", + "description": "Asset that defines KO Criteria Questions and Answers", + "partner": "traicie" + }, } diff --git a/config/type_defs/prompt_types.py b/config/type_defs/prompt_types.py index 769b3f2..a737549 100644 --- a/config/type_defs/prompt_types.py +++ b/config/type_defs/prompt_types.py @@ -36,4 +36,12 @@ PROMPT_TYPES = { "name": "translation_without_context", "description": "An assistant to translate text without context", }, + "check_affirmative_answer": { + "name": "check_affirmative_answer", + "description": "An assistant to check if the answer to a question is affirmative", + }, + "check_additional_information": { + "name": "check_additional_information", + "description": "An assistant to check if the answer to a question includes additional information or questions", + }, } diff --git a/config/type_defs/specialist_types.py b/config/type_defs/specialist_types.py index 7fc3ca7..67ac2d2 100644 --- a/config/type_defs/specialist_types.py +++ b/config/type_defs/specialist_types.py @@ -20,5 +20,9 @@ SPECIALIST_TYPES = { "TRAICIE_SELECTION_SPECIALIST": { "name": "Traicie Selection Specialist", "description": "Recruitment Selection Assistant", - } + }, + "TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST": { + "name": "Traicie KO Interview Definition Specialist", + "description": "Specialist assisting in questions and answers definition for KO Criteria", + }, } \ No newline at end of file diff --git a/documentation/CrewAI Specialist Implementation Guide.md b/documentation/CrewAI Specialist Implementation Guide.md new file mode 100644 index 0000000..42fe034 --- /dev/null +++ b/documentation/CrewAI Specialist Implementation Guide.md @@ -0,0 +1,32 @@ +# CrewAI Specialist Implementation Guide + +## Name Sensitivity +A lot of the required functionality to implement specialists has been automated. This automation is based on naming +conventions. So ... names of variables, attributes, ... needs to be precise, or you'll get problems. + +## Base Class: CrewAIBaseSpecialistExecutor + +Inherit your SpecialistExecutor class from the base class CrewAIBaseSpecialistExecutor + +### Conventions +- tasks are referenced by using the lower case name of the configured task +- agents idem dito + +### Specialise the __init__ method + +- Define the crews you want to use in your specialist implementation +- Do other initialisations you require +- Call super + +### Type and Typeversion properties + +- Adapt the type and type_version properties to define the correct specialist. This refers to the actual specialist configuration! + +### Implement _config_task_agents + +This method links the tasks to the agents that will perform LLM interactions. You can call the method _add_task_agent +to link both. + +### Implement _config_pydantic_outputs + +This method links the tasks to their pydantic outputs, and their name in the state class. diff --git a/eveai_app/views/interaction_forms.py b/eveai_app/views/interaction_forms.py index 53e48bf..6d9259c 100644 --- a/eveai_app/views/interaction_forms.py +++ b/eveai_app/views/interaction_forms.py @@ -102,35 +102,6 @@ class EditEveAIToolForm(BaseEditComponentForm): pass -class AddEveAIAssetForm(FlaskForm): - name = StringField('Name', validators=[DataRequired(), Length(max=50)]) - description = TextAreaField('Description', validators=[Optional()]) - type = SelectField('Type', validators=[DataRequired()]) - valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) - valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - types_dict = cache_manager.assets_types_cache.get_types() - self.type.choices = [(key, value['name']) for key, value in types_dict.items()] - - -class EditEveAIAssetForm(FlaskForm): - name = StringField('Name', validators=[DataRequired(), Length(max=50)]) - description = TextAreaField('Description', validators=[Optional()]) - type = SelectField('Type', validators=[DataRequired()], render_kw={'readonly': True}) - type_version = StringField('Type Version', validators=[DataRequired()], render_kw={'readonly': True}) - valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) - valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) - - -class EditEveAIAssetVersionForm(DynamicFormBase): - asset_name = StringField('Asset Name', validators=[DataRequired()], render_kw={'readonly': True}) - asset_type = StringField('Asset Type', validators=[DataRequired()], render_kw={'readonly': True}) - asset_type_version = StringField('Asset Type Version', validators=[DataRequired()], render_kw={'readonly': True}) - bucket_name = StringField('Bucket Name', validators=[DataRequired()], render_kw={'readonly': True}) - - class ExecuteSpecialistForm(DynamicFormBase): id = IntegerField('Specialist ID', validators=[DataRequired()], render_kw={'readonly': True}) name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True}) diff --git a/eveai_app/views/interaction_views.py b/eveai_app/views/interaction_views.py index 94f6455..21e3bba 100644 --- a/eveai_app/views/interaction_views.py +++ b/eveai_app/views/interaction_views.py @@ -14,12 +14,11 @@ from werkzeug.utils import secure_filename from common.models.document import Embedding, DocumentVersion, Retriever from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever, - EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion, SpecialistMagicLink) + EveAIAgent, EveAITask, EveAITool, EveAIAsset, SpecialistMagicLink) from common.extensions import db, cache_manager from common.models.user import SpecialistMagicLinkTenant from common.services.interaction.specialist_services import SpecialistServices -from common.utils.asset_utils import create_asset_stack, add_asset_version_file from common.utils.execution_progress import ExecutionProgressTracker from common.utils.model_logging_utils import set_logging_information, update_logging_information @@ -28,7 +27,7 @@ from common.utils.nginx_utils import prefixed_url_for from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm, - EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm, + EditEveAIToolForm, ExecuteSpecialistForm, SpecialistMagicLinkForm, EditSpecialistMagicLinkForm) interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction') @@ -486,92 +485,7 @@ def handle_tool_selection(): return redirect(prefixed_url_for('interaction_bp.edit_specialist')) -# Routes for Asset management --------------------------------------------------------------------- -@interaction_bp.route('/add_asset', methods=['GET', 'POST']) -@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') -def add_asset(): - form = AddEveAIAssetForm(request.form) - tenant_id = session.get('tenant').get('id') - - if form.validate_on_submit(): - try: - current_app.logger.info(f"Adding asset for tenant {tenant_id}") - - api_input = { - 'name': form.name.data, - 'description': form.description.data, - 'type': form.type.data, - 'valid_from': form.valid_from.data, - 'valid_to': form.valid_to.data, - } - new_asset, new_asset_version = create_asset_stack(api_input, tenant_id) - - return redirect(prefixed_url_for('interaction_bp.edit_asset_version', - asset_version_id=new_asset_version.id)) - except Exception as e: - current_app.logger.error(f'Failed to add asset for tenant {tenant_id}: {str(e)}') - flash('An error occurred while adding asset', 'error') - - return render_template('interaction/add_asset.html') - - -@interaction_bp.route('/edit_asset_version/', methods=['GET', 'POST']) -@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') -def edit_asset_version(asset_version_id): - asset_version = EveAIAssetVersion.query.get_or_404(asset_version_id) - form = EditEveAIAssetVersionForm(asset_version) - asset_config = cache_manager.assets_config_cache.get_config(asset_version.asset.type, - asset_version.asset.type_version) - form.add_dynamic_fields("configuration", asset_config, asset_version.configuration) - - if form.validate_on_submit(): - # Update the configuration dynamic fields - configuration = form.get_dynamic_data("configuration") - processed_configuration = {} - tenant_id = session.get('tenant').get('id') - # if files are returned, we will store the file_names in the configuration, and add the file to the appropriate - # bucket, in the appropriate location - for field_name, field_value in configuration.items(): - # Handle file field - check if the value is a FileStorage instance - if isinstance(field_value, FileStorage) and field_value.filename: - try: - # Upload file and retrieve object_name for the file - object_name = add_asset_version_file(asset_version, field_name, field_value, tenant_id) - - # Store object reference in configuration instead of file content - processed_configuration[field_name] = object_name - - except Exception as e: - current_app.logger.error(f"Failed to upload file for asset version {asset_version.id}: {str(e)}") - flash(f"Failed to upload file '{field_value.filename}': {str(e)}", "danger") - return render_template('interaction/edit_asset_version.html', form=form, - asset_version=asset_version) - # Handle normal fields - else: - processed_configuration[field_name] = field_value - - # Update the asset version with processed configuration - asset_version.configuration = processed_configuration - - # Update logging information - update_logging_information(asset_version, dt.now(tz.utc)) - - try: - db.session.commit() - flash('Asset uploaded successfully!', 'success') - current_app.logger.info(f'Asset Version {asset_version.id} updated successfully') - return redirect(prefixed_url_for('interaction_bp.assets')) - except SQLAlchemyError as e: - db.session.rollback() - flash(f'Failed to upload asset. Error: {str(e)}', 'danger') - current_app.logger.error(f'Failed to update asset version {asset_version.id}. Error: {str(e)}') - return render_template('interaction/edit_asset_version.html', form=form) - else: - form_validation_failed(request, form) - - return render_template('interaction/edit_asset_version.html', form=form) - - +# Specialist Execution ---------------------------------------------------------------------------- @interaction_bp.route('/execute_specialist/', methods=['GET', 'POST']) def execute_specialist(specialist_id): specialist = Specialist.query.get_or_404(specialist_id) @@ -603,6 +517,7 @@ def execute_specialist(specialist_id): return render_template('interaction/execute_specialist.html', form=form) +# Interaction Mgmt -------------------------------------------------------------------------------- @interaction_bp.route('/session_interactions_by_session_id/', methods=['GET']) @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def session_interactions_by_session_id(session_id): diff --git a/eveai_chat_client/static/assets/js/chat-app.js b/eveai_chat_client/static/assets/js/chat-app.js index 0fe302c..1b49b42 100644 --- a/eveai_chat_client/static/assets/js/chat-app.js +++ b/eveai_chat_client/static/assets/js/chat-app.js @@ -121,6 +121,8 @@ export const ChatApp = { // Load historical messages from server this.loadHistoricalMessages(); + console.log('Nr of messages:', this.allMessages.length); + // Add welcome message if no history if (this.allMessages.length === 0) { this.addWelcomeMessage(); @@ -157,12 +159,52 @@ export const ChatApp = { } }, - addWelcomeMessage() { - this.addMessage( - 'Hallo! Ik ben je AI assistant. Vraag gerust om een formulier zoals "contactformulier" of "bestelformulier"!', - 'ai', - 'text' - ); + async addWelcomeMessage() { + console.log('Sending initialize message to backend'); + + // Toon typing indicator + this.isTyping = true; + this.isLoading = true; + + try { + // Verzamel gegevens voor de API call + const apiData = { + message: 'Initialize', + conversation_id: this.conversationId, + user_id: this.userId, + language: this.currentLanguage + }; + + const response = await this.callAPI('/api/send_message', apiData); + + // Verberg typing indicator + this.isTyping = false; + + // Voeg AI response toe met task_id voor tracking + const aiMessage = this.addMessage( + '', + 'ai', + 'text' + ); + + // Voeg task_id toe als beschikbaar + if (response.task_id) { + console.log('Monitoring Initialize Task ID: ', response.task_id); + aiMessage.taskId = response.task_id; + } + } catch (error) { + console.error('Error sending initialize message:', error); + this.isTyping = false; + + // Voeg standaard welkomstbericht toe als fallback + this.addMessage( + 'Hallo! Ik ben je AI assistant. Vraag gerust om een formulier zoals "contactformulier" of "bestelformulier"!', + 'ai', + 'text' + ); + } finally { + this.isLoading = false; + } }, setupEventListeners() { diff --git a/eveai_chat_client/views/chat_views.py b/eveai_chat_client/views/chat_views.py index 96cf118..700f127 100644 --- a/eveai_chat_client/views/chat_views.py +++ b/eveai_chat_client/views/chat_views.py @@ -111,14 +111,26 @@ def chat(magic_link_code): if isinstance(specialist_config, str): specialist_config = json.loads(specialist_config) - welcome_message = specialist_config.get('welcome_message', 'Hello! How can I help you today?') + # # Send a first 'empty' message to the specialist, in order to receive a starting message + # Database(tenant_id).switch_schema() + # specialist_args = session['magic_link'].get('specialist_args', {}) + # specialist_args['question'] = '' + # result = SpecialistServices.execute_specialist( + # tenant_id=tenant_id, + # specialist_id=specialist.id, + # specialist_arguments=specialist_args, + # session_id=session['chat_session_id'], + # user_timezone=specialist_config.get('timezone', 'UTC') + # ) + # + # welcome_message = result.get('answer') return render_template('chat.html', tenant=tenant, tenant_make=tenant_make, specialist=specialist, customisation=customisation, - messages=[welcome_message], + messages=[], settings=settings, config=current_app.config ) diff --git a/eveai_chat_workers/__init__.py b/eveai_chat_workers/__init__.py index 21f563a..7bfeb96 100644 --- a/eveai_chat_workers/__init__.py +++ b/eveai_chat_workers/__init__.py @@ -4,7 +4,7 @@ from flask import Flask import os from common.utils.celery_utils import make_celery, init_celery -from common.extensions import db, cache_manager +from common.extensions import db, cache_manager, minio_client from config.logging_config import configure_logging from config.config import get_config @@ -43,6 +43,7 @@ def create_app(config_file=None): def register_extensions(app): db.init_app(app) cache_manager.init_app(app) + minio_client.init_app(app) def register_cache_handlers(app): diff --git a/eveai_chat_workers/outputs/globals/basic_types/list_item.py b/eveai_chat_workers/outputs/globals/basic_types/list_item.py index 4ed3993..14e576d 100644 --- a/eveai_chat_workers/outputs/globals/basic_types/list_item.py +++ b/eveai_chat_workers/outputs/globals/basic_types/list_item.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, Field + class ListItem(BaseModel): title: str = Field(..., description="The title or name of the item") - description: str = Field(..., description="A descriptive explanation of the item") \ No newline at end of file + description: str = Field(..., description="A descriptive explanation of the item") diff --git a/eveai_chat_workers/outputs/globals/q_a_output/q_a_output_v1_0.py b/eveai_chat_workers/outputs/globals/q_a_output/q_a_output_v1_0.py new file mode 100644 index 0000000..5f02e6b --- /dev/null +++ b/eveai_chat_workers/outputs/globals/q_a_output/q_a_output_v1_0.py @@ -0,0 +1,7 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class QAOutput(BaseModel): + answer: bool = Field(None, description="True or False") diff --git a/eveai_chat_workers/outputs/traicie/knockout_questions/knockout_questions_v1_0.py b/eveai_chat_workers/outputs/traicie/knockout_questions/knockout_questions_v1_0.py index 2d72ae2..3d55242 100644 --- a/eveai_chat_workers/outputs/traicie/knockout_questions/knockout_questions_v1_0.py +++ b/eveai_chat_workers/outputs/traicie/knockout_questions/knockout_questions_v1_0.py @@ -1,6 +1,7 @@ from typing import List, Optional + from pydantic import BaseModel, Field -from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem + class KOQuestion(BaseModel): title: str = Field(..., description="The title of the knockout criterium.") @@ -8,8 +9,37 @@ class KOQuestion(BaseModel): answer_positive: Optional[str] = Field(None, description="The answer to the question, resulting in a positive outcome.") answer_negative: Optional[str] = Field(None, description="The answer to the question, resulting in a negative outcome.") + @classmethod + def from_json(cls, json_str: str) -> 'KOQuestion': + """Deserialize from JSON string""" + return cls.model_validate_json(json_str) + + def to_json(self, **kwargs) -> str: + """Serialize to JSON string""" + return self.model_dump_json(**kwargs) + + class KOQuestions(BaseModel): ko_questions: List[KOQuestion] = Field( default_factory=list, description="KO Questions and answers." ) + + @classmethod + def from_json(cls, json_str: str) -> 'KOQuestions': + """Deserialize from JSON string""" + return cls.model_validate_json(json_str) + + def to_json(self, **kwargs) -> str: + """Serialize to JSON string""" + return self.model_dump_json(**kwargs) + + @classmethod + def from_question_list(cls, questions: List[KOQuestion]) -> 'KOQuestions': + """Create KOQuestions from a list of KOQuestion objects""" + return cls(ko_questions=questions) + + def to_question_list(self) -> List[KOQuestion]: + """Get the list of KOQuestion objects""" + return self.ko_questions + diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py b/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py new file mode 100644 index 0000000..e2c1d50 --- /dev/null +++ b/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py @@ -0,0 +1,308 @@ +from datetime import datetime as dt, timezone as tz +from typing import Optional, List, Dict + +import json +import yaml +from crewai.flow.flow import start, listen +from flask import current_app +from pydantic import BaseModel, Field + +from common.extensions import db, minio_client +from common.models.interaction import Specialist, EveAIAsset +from common.utils.eveai_exceptions import EveAISpecialistExecutionError +from common.utils.model_logging_utils import set_logging_information +from eveai_chat_workers.definitions.language_level.language_level_v1_0 import LANGUAGE_LEVEL +from eveai_chat_workers.definitions.tone_of_voice.tone_of_voice_v1_0 import TONE_OF_VOICE +from eveai_chat_workers.outputs.traicie.knockout_questions.knockout_questions_v1_0 import KOQuestions, KOQuestion +from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState +from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor +from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments + + +class SpecialistExecutor(CrewAIBaseSpecialistExecutor): + """ + type: TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST + type_version: 1.0 + Traicie Selection Specialist Executor class + """ + + def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs): + self.ko_def_crew = None + + super().__init__(tenant_id, specialist_id, session_id, task_id) + + + @property + def type(self) -> str: + return "TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST" + + @property + def type_version(self) -> str: + return "1.0" + + def _config_task_agents(self): + self._add_task_agent("traicie_ko_criteria_interview_definition_task", "traicie_recruiter_agent") + + def _config_pydantic_outputs(self): + self._add_pydantic_output("traicie_ko_criteria_interview_definition_task", KOQuestions, "ko_questions") + + def _config_state_result_relations(self): + self._add_state_result_relation("ko_questions") + + def _instantiate_specialist(self): + verbose = self.tuning + + ko_def_agents = [self.traicie_recruiter_agent] + ko_def_tasks = [self.traicie_ko_criteria_interview_definition_task] + self.ko_def_crew = EveAICrewAICrew( + self, + "KO Criteria Interview Definition Crew", + agents=ko_def_agents, + tasks=ko_def_tasks, + verbose=verbose, + ) + + self.flow = KOFlow( + self, + self.ko_def_crew + ) + + def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie KO Criteria Interview Definition Specialist execution started", {}) + + current_app.logger.debug(f"Arguments: {arguments.model_dump()}") + current_app.logger.debug(f"Formatted Context: {formatted_context}") + current_app.logger.debug(f"Formatted History: {self._formatted_history}") + current_app.logger.debug(f"Cached Chat Session: {self._cached_session}") + + if not self._cached_session.interactions: + specialist_phase = "initial" + else: + specialist_phase = self._cached_session.interactions[-1].specialist_results.get('phase', 'initial') + + results = None + + match specialist_phase: + case "initial": + results = self.execute_initial_state(arguments, formatted_context, citations) + + self.log_tuning(f"Traicie KO Criteria Interview Definition Specialist execution ended", + {"Results": results.model_dump() if results else "No info"}) + + return results + + + def execute_initial_state(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie KO Criteria Interview Definition Specialist initial_state_execution started", {}) + + selection_specialist = Specialist.query.get(arguments.specialist_id) + if not selection_specialist: + raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id, + "No selection specialist found") + if selection_specialist.type != "TRAICIE_SELECTION_SPECIALIST": + raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id, + "Specialist is no Selection Specialist") + + current_app.logger.debug(f"Specialist Competencies:\n" + f"{selection_specialist.configuration.get("competencies", [])}") + + ko_competencies = [] + for competency in selection_specialist.configuration.get("competencies", []): + if competency["is_knockout"] is True and competency["assess"] is True: + current_app.logger.debug(f"Assessable Knockout competency: {competency}") + ko_competencies.append({"title": competency["title"], "description": competency["description"]}) + + tone_of_voice = selection_specialist.configuration.get('tone_of_voice', 'Professional & Neutral') + selected_tone_of_voice = next( + (item for item in TONE_OF_VOICE if item["name"] == tone_of_voice), + None # fallback indien niet gevonden + ) + current_app.logger.debug(f"Selected tone of voice: {selected_tone_of_voice}") + tone_of_voice_context = f"{selected_tone_of_voice["description"]}" + + language_level = selection_specialist.configuration.get('language_level', 'Standard') + selected_language_level = next( + (item for item in LANGUAGE_LEVEL if item["name"] == language_level), + None + ) + current_app.logger.debug(f"Selected language level: {selected_language_level}") + language_level_context = (f"{selected_language_level['description']}, " + f"corresponding to CEFR level {selected_language_level['cefr_level']}") + + flow_inputs = { + 'tone_of_voice': tone_of_voice, + 'tone_of_voice_context': tone_of_voice_context, + 'language_level': language_level, + 'language_level_context': language_level_context, + 'ko_criteria': ko_competencies, + } + + flow_results = self.flow.kickoff(inputs=flow_inputs) + current_app.logger.debug(f"Flow results: {flow_results}") + current_app.logger.debug(f"Flow state: {self.flow.state}") + + new_type = "TRAICIE_KO_CRITERIA_QUESTIONS" + + current_app.logger.debug(f"KO Criteria Questions:\n {self.flow.state.ko_questions}") + # Controleer of we een KOQuestions object hebben of een lijst van KOQuestion objecten + if hasattr(self.flow.state.ko_questions, 'to_json'): + # Het is een KOQuestions object + json_str = self.flow.state.ko_questions.to_json() + elif isinstance(self.flow.state.ko_questions, list): + # Het is een lijst van KOQuestion objecten + # Maak een KOQuestions object en gebruik to_json daarop + ko_questions_obj = KOQuestions.from_question_list(self.flow.state.ko_questions) + json_str = ko_questions_obj.to_json() + else: + # Fallback voor het geval het een onverwacht type is + current_app.logger.warning(f"Unexpected type for ko_questions: {type(self.flow.state.ko_questions)}") + ko_questions_data = [q.model_dump() for q in self.flow.state.ko_questions] + json_str = json.dumps(ko_questions_data, ensure_ascii=False, indent=2) + + current_app.logger.debug(f"KO Criteria Questions json style:\n {json_str}") + + try: + asset = db.session.query(EveAIAsset).filter( + EveAIAsset.type == new_type, + EveAIAsset.type_version == "1.0.0", + EveAIAsset.configuration.is_not(None), + EveAIAsset.configuration.has_key('specialist_id'), + EveAIAsset.configuration['specialist_id'].astext.cast(db.Integer) == selection_specialist.id + ).first() + except (ValueError, TypeError) as e: + current_app.logger.warning(f"Error casting specialist_id in asset configuration: {str(e)}") + asset = None + + if not asset: + asset = EveAIAsset( + name=f"KO Criteria Form for specialist {selection_specialist.id}", + type=new_type, + type_version="1.0.0", + system_metadata={ + "Creator Specialist Type": self.type, + "Creator Specialist Type Version": self.type_version, + "Creator Specialist ID": self.specialist_id + }, + configuration={ + "specialist_id": selection_specialist.id, + }, + ) + set_logging_information(asset, dt.now(tz=tz.utc)) + asset.last_used_at = asset.created_at + else: + asset.last_used_at = dt.now(tz=tz.utc) + + try: + # Stap 1: Asset aanmaken maar nog niet committen + db.session.add(asset) + db.session.flush() # Geeft ons het ID zonder te committen + + # Stap 2: Upload naar MinIO (kan falen zonder database impact) + bucket_name, object_name, file_size = minio_client.upload_asset_file( + tenant_id=self.tenant_id, + asset_id=asset.id, + asset_type=new_type, + file_type="json", + file_data=json_str + ) + + # Stap 3: Storage metadata toevoegen + asset.bucket_name = bucket_name + asset.object_name = object_name + asset.file_size = file_size + asset.file_type = "json" + + # Stap 4: Token usage toevoegen + asset.prompt_tokens = self.ko_def_crew.usage_metrics.prompt_tokens + asset.completion_tokens = self.ko_def_crew.usage_metrics.completion_tokens + + # Alles in één keer committen + db.session.commit() + + except Exception as e: + current_app.logger.error(f"Error creating asset: {str(e)}") + db.session.rollback() + # Probeer MinIO cleanup als upload is gelukt maar database commit faalde + try: + if 'bucket_name' in locals() and 'object_name' in locals(): + minio_client.delete_object(bucket_name, object_name) + except: + pass # Log maar ga door met originele exception + raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id, + f"Failed to create asset: {str(e)}") + + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=f"asset {asset.id} created for specialist {selection_specialist.id}", + phase="finished", + asset_id=asset.id, + ) + + + return results + + +class KODefInput(BaseModel): + tone_of_voice: Optional[str] = Field(None, alias="tone_of_voice") + tone_of_voice_context: Optional[str] = Field(None, alias="tone_of_voice_context") + language_level: Optional[str] = Field(None, alias="language_level") + language_level_context: Optional[str] = Field(None, alias="language_level_context") + ko_criteria: Optional[List[Dict[str, str]]] = Field(None, alias="ko_criteria") + + +class KODefResult(SpecialistResult): + asset_id: Optional[int] = Field(None, alias="asset_id") + + +class KOFlowState(EveAIFlowState): + """Flow state for Traicie Role Definition specialist that automatically updates from task outputs""" + input: Optional[KODefInput] = None + ko_questions: Optional[List[KOQuestion]] = Field(None, alias="ko_questions") + phase: Optional[str] = Field(None, alias="phase") + + +class KOFlow(EveAICrewAIFlow[KOFlowState]): + def __init__(self, + specialist_executor: CrewAIBaseSpecialistExecutor, + ko_def_crew: EveAICrewAICrew, + **kwargs): + super().__init__(specialist_executor, "Traicie KO Interview Definiton Specialist Flow", **kwargs) + self.specialist_executor = specialist_executor + self.ko_def_crew = ko_def_crew + self.exception_raised = False + + @start() + def process_inputs(self): + return "" + + @listen(process_inputs) + async def execute_ko_def_definition(self): + inputs = self.state.input.model_dump() + try: + current_app.logger.debug("Run execute_ko_interview_definition") + crew_output = await self.ko_def_crew.kickoff_async(inputs=inputs) + # Unfortunately, crew_output will only contain the output of the latest task. + # As we will only take into account the flow state, we need to ensure both competencies and criteria + # are copies to the flow state. + update = {} + for task in self.ko_def_crew.tasks: + current_app.logger.debug(f"Task {task.name} output:\n{task.output}") + if task.name == "traicie_ko_criteria_interview_definition_task": + # update["competencies"] = task.output.pydantic.competencies + self.state.ko_questions = task.output.pydantic.ko_questions + # crew_output.pydantic = crew_output.pydantic.model_copy(update=update) + self.state.phase = "personal_contact_data" + current_app.logger.debug(f"State after execute_ko_def_definition: {self.state}") + current_app.logger.debug(f"State dump after execute_ko_def_definition: {self.state.model_dump()}") + return crew_output + except Exception as e: + current_app.logger.error(f"CREW execute_ko_def Kickoff Error: {str(e)}") + self.exception_raised = True + raise e + + async def kickoff_async(self, inputs=None): + current_app.logger.debug(f"Async kickoff {self.name}") + current_app.logger.debug(f"Inputs: {inputs}") + self.state.input = KODefInput.model_validate(inputs) + current_app.logger.debug(f"State: {self.state}") + result = await super().kickoff_async(inputs) + return self.state diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_3.py b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_3.py index d231145..1447712 100644 --- a/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_3.py +++ b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_3.py @@ -181,8 +181,8 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor): answer = f"Let's start our selection process by asking you a few important questions." if arguments.language != 'en': - TranslationServices.translate_config(ko_form, "fields", arguments.language) - TranslationServices.translate(answer, arguments.language) + TranslationServices.translate_config(self.tenant_id, ko_form, "fields", arguments.language) + TranslationServices.translate(self.tenant_id, answer, arguments.language) results = SpecialistResult.create_for_type(self.type, self.type_version, @@ -234,7 +234,8 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor): if arguments.language != 'nl': answer = TranslationServices.translate(answer, arguments.language) if arguments.language != 'en': - contact_form = TranslationServices.translate_config(contact_form, "fields", arguments.language) + contact_form = TranslationServices.translate_config(self.tenant_id, contact_form, "fields", + arguments.language) results = SpecialistResult.create_for_type(self.type, self.type_version, answer=answer, form_request=contact_form, diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py new file mode 100644 index 0000000..de868f7 --- /dev/null +++ b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py @@ -0,0 +1,370 @@ +import asyncio +import json +from os import wait +from typing import Optional, List, Dict, Any +from datetime import date +from time import sleep +from crewai.flow.flow import start, listen, and_ +from flask import current_app +from pydantic import BaseModel, Field, EmailStr +from sqlalchemy.exc import SQLAlchemyError + +from common.extensions import db +from common.models.user import Tenant +from common.models.interaction import Specialist +from common.services.utils.translation_services import TranslationServices +from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem +from eveai_chat_workers.outputs.traicie.knockout_questions.knockout_questions_v1_0 import KOQuestions, KOQuestion +from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor +from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments +from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies +from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState +from common.services.interaction.specialist_services import SpecialistServices +from common.extensions import cache_manager +from eveai_chat_workers.definitions.language_level.language_level_v1_0 import LANGUAGE_LEVEL +from eveai_chat_workers.definitions.tone_of_voice.tone_of_voice_v1_0 import TONE_OF_VOICE +from common.utils.eveai_exceptions import EveAISpecialistExecutionError + + +class SpecialistExecutor(CrewAIBaseSpecialistExecutor): + """ + type: TRAICIE_SELECTION_SPECIALIST + type_version: 1.1 + Traicie Selection Specialist Executor class + """ + + def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs): + self.role_definition_crew = None + + super().__init__(tenant_id, specialist_id, session_id, task_id) + + # Load the Tenant & set language + self.tenant = Tenant.query.get_or_404(tenant_id) + + @property + def type(self) -> str: + return "TRAICIE_SELECTION_SPECIALIST" + + @property + def type_version(self) -> str: + return "1.3" + + def _config_task_agents(self): + self._add_task_agent("traicie_ko_criteria_interview_definition_task", "traicie_recruiter_agent") + + def _config_pydantic_outputs(self): + self._add_pydantic_output("traicie_ko_criteria_interview_definition_task", KOQuestions, "ko_questions") + + def _config_state_result_relations(self): + self._add_state_result_relation("ko_criteria_questions") + self._add_state_result_relation("ko_criteria_scores") + self._add_state_result_relation("competency_questions") + self._add_state_result_relation("competency_scores") + self._add_state_result_relation("personal_contact_data") + + def _instantiate_specialist(self): + verbose = self.tuning + + ko_def_agents = [self.traicie_recruiter_agent] + ko_def_tasks = [self.traicie_ko_criteria_interview_definition_task] + self.ko_def_crew = EveAICrewAICrew( + self, + "KO Criteria Interview Definition Crew", + agents=ko_def_agents, + tasks=ko_def_tasks, + verbose=verbose, + ) + + self.flow = SelectionFlow( + self, + self.ko_def_crew + ) + + def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist execution started", {}) + + current_app.logger.debug(f"Arguments: {arguments.model_dump()}") + current_app.logger.debug(f"Formatted Context: {formatted_context}") + current_app.logger.debug(f"Formatted History: {self._formatted_history}") + current_app.logger.debug(f"Cached Chat Session: {self._cached_session}") + + if not self._cached_session.interactions: + specialist_phase = "initial" + else: + specialist_phase = self._cached_session.interactions[-1].specialist_results.get('phase', 'initial') + + results = None + + match specialist_phase: + case "initial": + results = self.execute_initial_state(arguments, formatted_context, citations) + case "ko_question_evaluation": + results = self.execute_ko_question_evaluation(arguments, formatted_context, citations) + case "personal_contact_data": + results = self.execute_personal_contact_data(arguments, formatted_context, citations) + case "no_valid_candidate": + results = self.execute_no_valid_candidate(arguments, formatted_context, citations) + case "candidate_selected": + results = self.execute_candidate_selected(arguments, formatted_context, citations) + + self.log_tuning(f"Traicie Selection Specialist execution ended", {"Results": results.model_dump() if results else "No info"}) + + return results + + + def execute_initial_state(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist initial_state execution started", {}) + + current_app.logger.debug(f"Specialist Competencies:\n{self.specialist.configuration.get("competencies", [])}") + + ko_competencies = [] + for competency in self.specialist.configuration.get("competencies", []): + if competency["is_knockout"] is True and competency["assess"] is True: + current_app.logger.debug(f"Assessable Knockout competency: {competency}") + ko_competencies.append({"title: ": competency["title"], "description": competency["description"]}) + + tone_of_voice = self.specialist.configuration.get('tone_of_voice', 'Professional & Neutral') + selected_tone_of_voice = next( + (item for item in TONE_OF_VOICE if item["name"] == tone_of_voice), + None # fallback indien niet gevonden + ) + current_app.logger.debug(f"Selected tone of voice: {selected_tone_of_voice}") + tone_of_voice_context = f"{selected_tone_of_voice["description"]}" + + language_level = self.specialist.configuration.get('language_level', 'Standard') + selected_language_level = next( + (item for item in LANGUAGE_LEVEL if item["name"] == language_level), + None + ) + current_app.logger.debug(f"Selected language level: {selected_language_level}") + language_level_context = (f"{selected_language_level['description']}, " + f"corresponding to CEFR level {selected_language_level['cefr_level']}") + + flow_inputs = { + "region": arguments.region, + "working_schedule": arguments.working_schedule, + "start_date": arguments.start_date, + "language": arguments.language, + "interaction_mode": arguments.interaction_mode, + 'tone_of_voice': tone_of_voice, + 'tone_of_voice_context': tone_of_voice_context, + 'language_level': language_level, + 'language_level_context': language_level_context, + 'ko_criteria': ko_competencies, + } + + flow_results = self.flow.kickoff(inputs=flow_inputs) + + current_app.logger.debug(f"Flow results: {flow_results}") + + current_app.logger.debug(f"Flow state: {self.flow.state}") + + fields = {} + for ko_question in self.flow.state.ko_criteria_questions: + fields[ko_question.title] = { + "name": ko_question.title, + "description": ko_question.title, + "context": ko_question.question, + "type": "options", + "required": True, + "allowed_values": [ko_question.answer_positive, ko_question.answer_negative] + } + + ko_form = { + "type": "KO_CRITERIA_FORM", + "version": "1.0.0", + "name": "Starter Questions", + "icon": "verified", + "fields": fields, + } + + answer = f"Let's start our selection process by asking you a few important questions." + + if arguments.language != 'en': + TranslationServices.translate_config(self.tenant_id, ko_form, "fields", arguments.language) + TranslationServices.translate(self.tenant_id, answer, arguments.language) + + + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=answer, + form_request=ko_form, + phase="ko_question_evaluation") + + return results + + def execute_ko_question_evaluation(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist ko_question_evaluation started", {}) + + # Check if the form has been returned (it should) + if not arguments.form_values: + raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id, "No form values returned") + current_app.logger.debug(f"Form values: {arguments.form_values}") + + # Load the previous KO Questions + previous_ko_questions = self.flow.state.ko_criteria_questions + current_app.logger.debug(f"Previous KO Questions: {previous_ko_questions}") + + # Evaluate KO Criteria + evaluation = "positive" + for criterium, answer in arguments.form_values.items(): + for qa in previous_ko_questions: + if qa.get("title") == criterium: + if qa.get("answer_positive") != answer: + evaluation = "negative" + break + if evaluation == "negative": + break + + if evaluation == "negative": + answer = (f"We hebben de antwoorden op onze eerste vragen verwerkt. Je voldoet jammer genoeg niet aan de " + f"minimale vereisten voor deze job.") + if arguments.language != 'nl': + answer = TranslationServices.translate(answer, arguments.language) + + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=answer, + form_request=None, + phase="no_valid_candidate") + else: + answer = (f"We hebben de antwoorden op de KO criteria verwerkt. Je bent een geschikte kandidaat. " + f"Ben je bereid je contactgegevens door te geven, zodat we je kunnen contacteren voor een verder " + f"gesprek?") + # Check if answers to questions are positive + contact_form = cache_manager.specialist_forms_config_cache.get_config("PERSONAL_CONTACT_FORM", "1.0") + if arguments.language != 'nl': + answer = TranslationServices.translate(answer, arguments.language) + if arguments.language != 'en': + contact_form = TranslationServices.translate_config(self.tenant_id, contact_form, "fields", arguments.language) + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=answer, + form_request=contact_form, + phase="personal_contact_data") + + return results + + def execute_personal_contact_data(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist personal_contact_data started", {}) + + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=f"We hebben de contactgegevens verwerkt. We nemen zo snel mogelijk contact met je op.", + phase="candidate_selected") + return results + + def execute_no_valid_candidate(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist no_valid_candidate started", {}) + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=f"Je voldoet jammer genoeg niet aan de minimale vereisten voor deze job. Maar solliciteer gerust voor één van onze andere jobs.", + phase="no_valid_candidate") + + def execute_candidate_selected(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist candidate_selected started", {}) + results = SpecialistResult.create_for_type(self.type, self.type_version, + answer=f"We hebben je contactgegegevens verwerkt. We nemen zo snel mogelijk contact met je op.", + phase="candidate_selected") + return results + + +class SelectionInput(BaseModel): + region: str = Field(..., alias="region") + working_schedule: Optional[str] = Field(..., alias="working_schedule") + start_date: Optional[date] = Field(None, alias="vacancy_text") + language: Optional[str] = Field(None, alias="language") + interaction_mode: Optional[str] = Field(None, alias="interaction_mode") + tone_of_voice: Optional[str] = Field(None, alias="tone_of_voice") + tone_of_voice_context: Optional[str] = Field(None, alias="tone_of_voice_context") + language_level: Optional[str] = Field(None, alias="language_level") + language_level_context: Optional[str] = Field(None, alias="language_level_context") + ko_criteria: Optional[List[Dict[str, str]]] = Field(None, alias="ko_criteria") + question: Optional[str] = Field(None, alias="question") + field_values: Optional[Dict[str, Any]] = Field(None, alias="field_values") + + +class SelectionKOCriteriumScore(BaseModel): + criterium: Optional[str] = Field(None, alias="criterium") + answer: Optional[str] = Field(None, alias="answer") + score: Optional[int] = Field(None, alias="score") + + +class SelectionCompetencyScore(BaseModel): + competency: Optional[str] = Field(None, alias="competency") + answer: Optional[str] = Field(None, alias="answer") + score: Optional[int] = Field(None, alias="score") + + +class PersonalContactData(BaseModel): + name: str = Field(..., description="Your name", alias="name") + email: EmailStr = Field(..., description="Your Name", alias="email") + phone: str = Field(..., description="Your Phone Number", alias="phone") + address: Optional[str] = Field(None, description="Your Address", alias="address") + zip: Optional[str] = Field(None, description="Postal Code", alias="zip") + city: Optional[str] = Field(None, description="City", alias="city") + country: Optional[str] = Field(None, description="Country", alias="country") + consent: bool = Field(..., description="Consent", alias="consent") + + +class SelectionResult(SpecialistResult): + ko_criteria_questions: Optional[List[ListItem]] = Field(None, alias="ko_criteria_questions") + ko_criteria_scores: Optional[List[SelectionKOCriteriumScore]] = Field(None, alias="ko_criteria_scores") + competency_questions: Optional[List[ListItem]] = Field(None, alias="competency_questions") + competency_scores: Optional[List[SelectionCompetencyScore]] = Field(None, alias="competency_scores") + personal_contact_data: Optional[PersonalContactData] = Field(None, alias="personal_contact_data") + + +class SelectionFlowState(EveAIFlowState): + """Flow state for Traicie Role Definition specialist that automatically updates from task outputs""" + input: Optional[SelectionInput] = None + ko_criteria_questions: Optional[List[KOQuestion]] = Field(None, alias="ko_criteria_questions") + ko_criteria_scores: Optional[List[SelectionKOCriteriumScore]] = Field(None, alias="ko_criteria_scores") + competency_questions: Optional[List[ListItem]] = Field(None, alias="competency_questions") + competency_scores: Optional[List[SelectionCompetencyScore]] = Field(None, alias="competency_scores") + personal_contact_data: Optional[PersonalContactData] = Field(None, alias="personal_contact_data") + phase: Optional[str] = Field(None, alias="phase") + interaction_mode: Optional[str] = Field(None, alias="mode") + + +class SelectionFlow(EveAICrewAIFlow[SelectionFlowState]): + def __init__(self, + specialist_executor: CrewAIBaseSpecialistExecutor, + ko_def_crew: EveAICrewAICrew, + **kwargs): + super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs) + self.specialist_executor = specialist_executor + self.ko_def_crew = ko_def_crew + self.exception_raised = False + + @start() + def process_inputs(self): + return "" + + @listen(process_inputs) + async def execute_ko_def_definition(self): + inputs = self.state.input.model_dump() + try: + current_app.logger.debug("execute_ko_interview_definition") + crew_output = await self.ko_def_crew.kickoff_async(inputs=inputs) + # Unfortunately, crew_output will only contain the output of the latest task. + # As we will only take into account the flow state, we need to ensure both competencies and criteria + # are copies to the flow state. + update = {} + for task in self.ko_def_crew.tasks: + current_app.logger.debug(f"Task {task.name} output:\n{task.output}") + if task.name == "traicie_ko_criteria_interview_definition_task": + # update["competencies"] = task.output.pydantic.competencies + self.state.ko_criteria_questions = task.output.pydantic.ko_questions + # crew_output.pydantic = crew_output.pydantic.model_copy(update=update) + self.state.phase = "personal_contact_data" + current_app.logger.debug(f"State after execute_ko_def_definition: {self.state}") + current_app.logger.debug(f"State dump after execute_ko_def_definition: {self.state.model_dump()}") + return crew_output + except Exception as e: + current_app.logger.error(f"CREW execute_ko_def Kickoff Error: {str(e)}") + self.exception_raised = True + raise e + + async def kickoff_async(self, inputs=None): + current_app.logger.debug(f"Async kickoff {self.name}") + current_app.logger.debug(f"Inputs: {inputs}") + self.state.input = SelectionInput.model_validate(inputs) + current_app.logger.debug(f"State: {self.state}") + result = await super().kickoff_async(inputs) + return self.state diff --git a/migrations/tenant/env.py b/migrations/tenant/env.py index 643c3af..9cb8044 100644 --- a/migrations/tenant/env.py +++ b/migrations/tenant/env.py @@ -73,7 +73,7 @@ def get_public_table_names(): return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage', 'business_event_log', 'tenant_project', 'partner', 'partner_service', 'invoice', 'license_period', 'license_change_log', 'partner_service_license_tier', 'payment', 'partner_tenant', 'tenant_make', - 'specialist_magic_link_tenant'] + 'specialist_magic_link_tenant', 'translation_cache'] PUBLIC_TABLES = get_public_table_names() logger.info(f"Public tables: {PUBLIC_TABLES}") diff --git a/migrations/tenant/versions/af3d56001771_eveaiasset_changes_removing_.py b/migrations/tenant/versions/af3d56001771_eveaiasset_changes_removing_.py new file mode 100644 index 0000000..7c50ad2 --- /dev/null +++ b/migrations/tenant/versions/af3d56001771_eveaiasset_changes_removing_.py @@ -0,0 +1,43 @@ +"""EveAIAsset changes, removing EveAIAssetVersion and other models + +Revision ID: af3d56001771 +Revises: b1647f31339a +Create Date: 2025-07-02 07:58:10.689637 + +""" +from alembic import op +import sqlalchemy as sa +import pgvector +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'af3d56001771' +down_revision = 'b1647f31339a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('eve_ai_asset_instruction') + op.drop_table('eve_ai_processed_asset') + op.drop_table('eve_ai_asset_version') + op.add_column('eve_ai_asset', sa.Column('bucket_name', sa.String(length=255), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('object_name', sa.String(length=200), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('file_type', sa.String(length=20), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('file_size', sa.Float(), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('user_metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('system_metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('configuration', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('prompt_tokens', sa.Integer(), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('completion_tokens', sa.Integer(), nullable=True)) + op.add_column('eve_ai_asset', sa.Column('last_used_at', sa.DateTime(), nullable=True)) + op.drop_column('eve_ai_asset', 'valid_to') + op.drop_column('eve_ai_asset', 'valid_from') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ###