- Introduction of TRACIE_KO_INTERVIEW_DEFINITION_SPECIALIST
- Re-introduction of EveAIAsset - Make translation services resistent for situation with and without current_event defined. - Ensure first question is asked in eveai_chat_client - Start of version 1.4.0 of TRAICIE_SELECTION_SPECIALIST
This commit is contained in:
@@ -67,25 +67,23 @@ class EveAIAsset(db.Model):
|
|||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
type = db.Column(db.String(50), nullable=False, default="DOCUMENT_TEMPLATE")
|
type = db.Column(db.String(50), nullable=False, default="DOCUMENT_TEMPLATE")
|
||||||
type_version = db.Column(db.String(20), nullable=True, default="1.0.0")
|
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
|
# Storage 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)
|
|
||||||
bucket_name = db.Column(db.String(255), nullable=True)
|
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)
|
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
|
# Versioning Information
|
||||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
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_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))
|
updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
|
||||||
|
|
||||||
# Relations
|
last_used_at = db.Column(db.DateTime, nullable=True)
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
class EveAIAgent(db.Model):
|
class EveAIAgent(db.Model):
|
||||||
|
|||||||
9
common/services/interaction/asset_services.py
Normal file
9
common/services/interaction/asset_services.py
Normal file
@@ -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)
|
||||||
65
common/services/utils/answer_check_services.py
Normal file
65
common/services/utils/answer_check_services.py
Normal file
@@ -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
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from flask import session
|
||||||
|
|
||||||
from common.extensions import cache_manager
|
from common.extensions import cache_manager
|
||||||
from common.utils.business_event import BusinessEvent
|
from common.utils.business_event import BusinessEvent
|
||||||
from common.utils.business_event_context import current_event
|
from common.utils.business_event_context import current_event
|
||||||
@@ -7,11 +10,13 @@ from common.utils.business_event_context import current_event
|
|||||||
class TranslationServices:
|
class TranslationServices:
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
Vertaalt een configuratie op basis van een veld-configuratie.
|
||||||
|
|
||||||
Args:
|
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
|
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')
|
field_config: De naam van een veld-configuratie (bijv. 'fields')
|
||||||
target_language: De taal waarnaar vertaald moet worden
|
target_language: De taal waarnaar vertaald moet worden
|
||||||
@@ -21,6 +26,26 @@ class TranslationServices:
|
|||||||
Returns:
|
Returns:
|
||||||
Een dictionary met de vertaalde configuratie
|
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
|
# Zorg ervoor dat we een dictionary hebben
|
||||||
if isinstance(config_data, str):
|
if isinstance(config_data, str):
|
||||||
config_data = json.loads(config_data)
|
config_data = json.loads(config_data)
|
||||||
@@ -31,78 +56,79 @@ class TranslationServices:
|
|||||||
# Haal type en versie op voor de Business Event span
|
# Haal type en versie op voor de Business Event span
|
||||||
config_type = config_data.get('type', 'Unknown')
|
config_type = config_data.get('type', 'Unknown')
|
||||||
config_version = config_data.get('version', 'Unknown')
|
config_version = config_data.get('version', 'Unknown')
|
||||||
span_name = f"{config_type}-{config_version}-{field_config}"
|
|
||||||
|
|
||||||
# Start een Business Event context
|
if field_config in config_data:
|
||||||
with BusinessEvent('Config Translation Service', 0):
|
fields = config_data[field_config]
|
||||||
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]
|
|
||||||
|
|
||||||
# Haal description uit metadata voor context als geen context is opgegeven
|
# Haal description uit metadata voor context als geen context is opgegeven
|
||||||
description_context = ""
|
description_context = ""
|
||||||
if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
|
if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
|
||||||
description_context = config_data['metadata']['description']
|
description_context = config_data['metadata']['description']
|
||||||
|
|
||||||
# Loop door elk veld in de configuratie
|
# Loop door elk veld in de configuratie
|
||||||
for field_name, field_data in fields.items():
|
for field_name, field_data in fields.items():
|
||||||
# Vertaal name als het bestaat en niet leeg is
|
# Vertaal name als het bestaat en niet leeg is
|
||||||
if 'name' in field_data and field_data['name']:
|
if 'name' in field_data and field_data['name']:
|
||||||
# Gebruik context indien opgegeven, anders description_context
|
# Gebruik context indien opgegeven, anders description_context
|
||||||
field_context = context if context else description_context
|
field_context = context if context else description_context
|
||||||
translated_name = cache_manager.translation_cache.get_translation(
|
translated_name = cache_manager.translation_cache.get_translation(
|
||||||
text=field_data['name'],
|
text=field_data['name'],
|
||||||
target_lang=target_language,
|
target_lang=target_language,
|
||||||
source_lang=source_language,
|
source_lang=source_language,
|
||||||
context=field_context
|
context=field_context
|
||||||
)
|
)
|
||||||
if translated_name:
|
if translated_name:
|
||||||
translated_config[field_config][field_name]['name'] = translated_name.translated_text
|
translated_config[field_config][field_name]['name'] = translated_name.translated_text
|
||||||
|
|
||||||
if 'title' in field_data and field_data['title']:
|
if 'title' in field_data and field_data['title']:
|
||||||
# Gebruik context indien opgegeven, anders description_context
|
# Gebruik context indien opgegeven, anders description_context
|
||||||
field_context = context if context else description_context
|
field_context = context if context else description_context
|
||||||
translated_title = cache_manager.translation_cache.get_translation(
|
translated_title = cache_manager.translation_cache.get_translation(
|
||||||
text=field_data['title'],
|
text=field_data['title'],
|
||||||
target_lang=target_language,
|
target_lang=target_language,
|
||||||
source_lang=source_language,
|
source_lang=source_language,
|
||||||
context=field_context
|
context=field_context
|
||||||
)
|
)
|
||||||
if translated_title:
|
if translated_title:
|
||||||
translated_config[field_config][field_name]['title'] = translated_title.translated_text
|
translated_config[field_config][field_name]['title'] = translated_title.translated_text
|
||||||
|
|
||||||
# Vertaal description als het bestaat en niet leeg is
|
# Vertaal description als het bestaat en niet leeg is
|
||||||
if 'description' in field_data and field_data['description']:
|
if 'description' in field_data and field_data['description']:
|
||||||
# Gebruik context indien opgegeven, anders description_context
|
# Gebruik context indien opgegeven, anders description_context
|
||||||
field_context = context if context else description_context
|
field_context = context if context else description_context
|
||||||
translated_desc = cache_manager.translation_cache.get_translation(
|
translated_desc = cache_manager.translation_cache.get_translation(
|
||||||
text=field_data['description'],
|
text=field_data['description'],
|
||||||
target_lang=target_language,
|
target_lang=target_language,
|
||||||
source_lang=source_language,
|
source_lang=source_language,
|
||||||
context=field_context
|
context=field_context
|
||||||
)
|
)
|
||||||
if translated_desc:
|
if translated_desc:
|
||||||
translated_config[field_config][field_name]['description'] = translated_desc.translated_text
|
translated_config[field_config][field_name]['description'] = translated_desc.translated_text
|
||||||
|
|
||||||
# Vertaal context als het bestaat en niet leeg is
|
# Vertaal context als het bestaat en niet leeg is
|
||||||
if 'context' in field_data and field_data['context']:
|
if 'context' in field_data and field_data['context']:
|
||||||
translated_ctx = cache_manager.translation_cache.get_translation(
|
translated_ctx = cache_manager.translation_cache.get_translation(
|
||||||
text=field_data['context'],
|
text=field_data['context'],
|
||||||
target_lang=target_language,
|
target_lang=target_language,
|
||||||
source_lang=source_language,
|
source_lang=source_language,
|
||||||
context=context
|
context=context
|
||||||
)
|
)
|
||||||
if translated_ctx:
|
if translated_ctx:
|
||||||
translated_config[field_config][field_name]['context'] = translated_ctx.translated_text
|
translated_config[field_config][field_name]['context'] = translated_ctx.translated_text
|
||||||
|
|
||||||
return translated_config
|
return translated_config
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
context: Optional[str] = None)-> str:
|
||||||
with BusinessEvent('Translation Service', 0):
|
if current_event:
|
||||||
with current_event.create_span('Translation'):
|
with current_event.create_span('Translation'):
|
||||||
translation_cache = cache_manager.translation_cache.get_translation(text, target_language,
|
translation_cache = cache_manager.translation_cache.get_translation(text, target_language,
|
||||||
source_language, context)
|
source_language, context)
|
||||||
return translation_cache.translated_text
|
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
|
||||||
@@ -4,59 +4,9 @@ from flask import current_app
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from common.extensions import cache_manager, minio_client, db
|
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
|
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ class MinioClient:
|
|||||||
def generate_object_name(self, document_id, language, version_id, filename):
|
def generate_object_name(self, document_id, language, version_id, filename):
|
||||||
return f"{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):
|
def generate_asset_name(self, asset_id, asset_type, content_type):
|
||||||
return f"assets/{asset_version_id}/{file_name}.{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):
|
def upload_document_file(self, tenant_id, document_id, language, version_id, filename, file_data):
|
||||||
bucket_name = self.generate_bucket_name(tenant_id)
|
bucket_name = self.generate_bucket_name(tenant_id)
|
||||||
@@ -57,8 +57,10 @@ class MinioClient:
|
|||||||
except S3Error as err:
|
except S3Error as err:
|
||||||
raise Exception(f"Error occurred while uploading file: {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):
|
def upload_asset_file(self, tenant_id: int, asset_id: int, asset_type: str, file_type: str,
|
||||||
object_name = self.generate_asset_name(asset_version_id, file_name, file_type)
|
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:
|
try:
|
||||||
if isinstance(file_data, FileStorage):
|
if isinstance(file_data, FileStorage):
|
||||||
@@ -73,7 +75,7 @@ class MinioClient:
|
|||||||
self.client.put_object(
|
self.client.put_object(
|
||||||
bucket_name, object_name, io.BytesIO(file_data), len(file_data)
|
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:
|
except S3Error as err:
|
||||||
raise Exception(f"Error occurred while uploading asset: {err}")
|
raise Exception(f"Error occurred while uploading asset: {err}")
|
||||||
|
|
||||||
@@ -84,6 +86,13 @@ class MinioClient:
|
|||||||
except S3Error as err:
|
except S3Error as err:
|
||||||
raise Exception(f"Error occurred while downloading file: {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):
|
def list_document_files(self, tenant_id, document_id, language=None, version_id=None):
|
||||||
bucket_name = self.generate_bucket_name(tenant_id)
|
bucket_name = self.generate_bucket_name(tenant_id)
|
||||||
prefix = f"{document_id}/"
|
prefix = f"{document_id}/"
|
||||||
@@ -105,3 +114,9 @@ class MinioClient:
|
|||||||
return True
|
return True
|
||||||
except S3Error as err:
|
except S3Error as err:
|
||||||
raise Exception(f"Error occurred while deleting file: {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}")
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
13
config/prompts/globals/check_affirmative_answer/1.0.0.yaml
Normal file
13
config/prompts/globals/check_affirmative_answer/1.0.0.yaml
Normal file
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -2,7 +2,7 @@ version: "1.0.0"
|
|||||||
name: "Traicie Selection Specialist"
|
name: "Traicie Selection Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
partner: "traicie"
|
partner: "traicie"
|
||||||
chat: false
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
@@ -111,4 +111,4 @@ metadata:
|
|||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2025-05-27"
|
date_added: "2025-05-27"
|
||||||
changes: "Updated for unified competencies and ko criteria"
|
changes: "Updated for unified competencies and ko criteria"
|
||||||
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
description: "Assistant to assist in candidate selection"
|
||||||
@@ -2,7 +2,7 @@ version: "1.1.0"
|
|||||||
name: "Traicie Selection Specialist"
|
name: "Traicie Selection Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
partner: "traicie"
|
partner: "traicie"
|
||||||
chat: false
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
@@ -117,4 +117,4 @@ metadata:
|
|||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2025-05-27"
|
date_added: "2025-05-27"
|
||||||
changes: "Add make to the selection specialist"
|
changes: "Add make to the selection specialist"
|
||||||
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
description: "Assistant to assist in candidate selection"
|
||||||
@@ -2,7 +2,7 @@ version: "1.3.0"
|
|||||||
name: "Traicie Selection Specialist"
|
name: "Traicie Selection Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
partner: "traicie"
|
partner: "traicie"
|
||||||
chat: false
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
@@ -117,4 +117,4 @@ metadata:
|
|||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2025-06-16"
|
date_added: "2025-06-16"
|
||||||
changes: "Realising the actual interaction with the LLM"
|
changes: "Realising the actual interaction with the LLM"
|
||||||
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
description: "Assistant to assist in candidate selection"
|
||||||
@@ -2,7 +2,7 @@ version: "1.3.0"
|
|||||||
name: "Traicie Selection Specialist"
|
name: "Traicie Selection Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
partner: "traicie"
|
partner: "traicie"
|
||||||
chat: false
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
@@ -117,4 +117,4 @@ metadata:
|
|||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2025-06-18"
|
date_added: "2025-06-18"
|
||||||
changes: "Add make to the selection specialist"
|
changes: "Add make to the selection specialist"
|
||||||
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
description: "Assistant to assist in candidate selection"
|
||||||
@@ -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"
|
||||||
@@ -11,7 +11,7 @@ task_description: >
|
|||||||
|
|
||||||
Apply the following tone of voice in both questions and answers: {tone_of_voice}
|
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}
|
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}```
|
```{competencies}```
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ task_description: >
|
|||||||
|
|
||||||
Apply the following language level in both questions and answers: {language_level}, i.e. {language_level_context}
|
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}```
|
```{ko_criteria}```
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ task_description: >
|
|||||||
expected_output: >
|
expected_output: >
|
||||||
For each of the ko criteria, you provide:
|
For each of the ko criteria, you provide:
|
||||||
- the exact title as specified in the original language
|
- the exact title as specified in the original language
|
||||||
- the question in {language}
|
- the question in the original language
|
||||||
- a positive answer, resulting in a positive evaluation of the criterium. In {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 {language}.
|
- a negative answer, resulting in a negative evaluation of the criterium, in the original langauge.
|
||||||
{custom_expected_output}
|
{custom_expected_output}
|
||||||
metadata:
|
metadata:
|
||||||
author: "Josako"
|
author: "Josako"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Agent Types
|
# Agent Types
|
||||||
AGENT_TYPES = {
|
ASSET_TYPES = {
|
||||||
"DOCUMENT_TEMPLATE": {
|
"DOCUMENT_TEMPLATE": {
|
||||||
"name": "Document Template",
|
"name": "Document Template",
|
||||||
"description": "Asset that defines a template in markdown a specialist can process",
|
"description": "Asset that defines a template in markdown a specialist can process",
|
||||||
@@ -8,4 +8,9 @@ AGENT_TYPES = {
|
|||||||
"name": "Specialist Configuration",
|
"name": "Specialist Configuration",
|
||||||
"description": "Asset that defines a 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"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,4 +36,12 @@ PROMPT_TYPES = {
|
|||||||
"name": "translation_without_context",
|
"name": "translation_without_context",
|
||||||
"description": "An assistant to translate text 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",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,9 @@ SPECIALIST_TYPES = {
|
|||||||
"TRAICIE_SELECTION_SPECIALIST": {
|
"TRAICIE_SELECTION_SPECIALIST": {
|
||||||
"name": "Traicie Selection Specialist",
|
"name": "Traicie Selection Specialist",
|
||||||
"description": "Recruitment Selection Assistant",
|
"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",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
32
documentation/CrewAI Specialist Implementation Guide.md
Normal file
32
documentation/CrewAI Specialist Implementation Guide.md
Normal file
@@ -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.
|
||||||
@@ -102,35 +102,6 @@ class EditEveAIToolForm(BaseEditComponentForm):
|
|||||||
pass
|
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):
|
class ExecuteSpecialistForm(DynamicFormBase):
|
||||||
id = IntegerField('Specialist ID', validators=[DataRequired()], render_kw={'readonly': True})
|
id = IntegerField('Specialist ID', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
|
name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ from werkzeug.utils import secure_filename
|
|||||||
|
|
||||||
from common.models.document import Embedding, DocumentVersion, Retriever
|
from common.models.document import Embedding, DocumentVersion, Retriever
|
||||||
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
|
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.extensions import db, cache_manager
|
||||||
from common.models.user import SpecialistMagicLinkTenant
|
from common.models.user import SpecialistMagicLinkTenant
|
||||||
from common.services.interaction.specialist_services import SpecialistServices
|
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.execution_progress import ExecutionProgressTracker
|
||||||
from common.utils.model_logging_utils import set_logging_information, update_logging_information
|
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 common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||||
|
|
||||||
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
||||||
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm,
|
EditEveAIToolForm, ExecuteSpecialistForm,
|
||||||
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm)
|
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm)
|
||||||
|
|
||||||
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
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'))
|
return redirect(prefixed_url_for('interaction_bp.edit_specialist'))
|
||||||
|
|
||||||
|
|
||||||
# Routes for Asset management ---------------------------------------------------------------------
|
# Specialist Execution ----------------------------------------------------------------------------
|
||||||
@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/<int:asset_version_id>', 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)
|
|
||||||
|
|
||||||
|
|
||||||
@interaction_bp.route('/execute_specialist/<int:specialist_id>', methods=['GET', 'POST'])
|
@interaction_bp.route('/execute_specialist/<int:specialist_id>', methods=['GET', 'POST'])
|
||||||
def execute_specialist(specialist_id):
|
def execute_specialist(specialist_id):
|
||||||
specialist = Specialist.query.get_or_404(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)
|
return render_template('interaction/execute_specialist.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
# Interaction Mgmt --------------------------------------------------------------------------------
|
||||||
@interaction_bp.route('/session_interactions_by_session_id/<session_id>', methods=['GET'])
|
@interaction_bp.route('/session_interactions_by_session_id/<session_id>', methods=['GET'])
|
||||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
def session_interactions_by_session_id(session_id):
|
def session_interactions_by_session_id(session_id):
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ export const ChatApp = {
|
|||||||
// Load historical messages from server
|
// Load historical messages from server
|
||||||
this.loadHistoricalMessages();
|
this.loadHistoricalMessages();
|
||||||
|
|
||||||
|
console.log('Nr of messages:', this.allMessages.length);
|
||||||
|
|
||||||
// Add welcome message if no history
|
// Add welcome message if no history
|
||||||
if (this.allMessages.length === 0) {
|
if (this.allMessages.length === 0) {
|
||||||
this.addWelcomeMessage();
|
this.addWelcomeMessage();
|
||||||
@@ -157,12 +159,52 @@ export const ChatApp = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addWelcomeMessage() {
|
async addWelcomeMessage() {
|
||||||
this.addMessage(
|
console.log('Sending initialize message to backend');
|
||||||
'Hallo! Ik ben je AI assistant. Vraag gerust om een formulier zoals "contactformulier" of "bestelformulier"!',
|
|
||||||
'ai',
|
// Toon typing indicator
|
||||||
'text'
|
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() {
|
setupEventListeners() {
|
||||||
|
|||||||
@@ -111,14 +111,26 @@ def chat(magic_link_code):
|
|||||||
if isinstance(specialist_config, str):
|
if isinstance(specialist_config, str):
|
||||||
specialist_config = json.loads(specialist_config)
|
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',
|
return render_template('chat.html',
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
tenant_make=tenant_make,
|
tenant_make=tenant_make,
|
||||||
specialist=specialist,
|
specialist=specialist,
|
||||||
customisation=customisation,
|
customisation=customisation,
|
||||||
messages=[welcome_message],
|
messages=[],
|
||||||
settings=settings,
|
settings=settings,
|
||||||
config=current_app.config
|
config=current_app.config
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from flask import Flask
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from common.utils.celery_utils import make_celery, init_celery
|
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.logging_config import configure_logging
|
||||||
from config.config import get_config
|
from config.config import get_config
|
||||||
|
|
||||||
@@ -43,6 +43,7 @@ def create_app(config_file=None):
|
|||||||
def register_extensions(app):
|
def register_extensions(app):
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
cache_manager.init_app(app)
|
cache_manager.init_app(app)
|
||||||
|
minio_client.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
def register_cache_handlers(app):
|
def register_cache_handlers(app):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class ListItem(BaseModel):
|
class ListItem(BaseModel):
|
||||||
title: str = Field(..., description="The title or name of the item")
|
title: str = Field(..., description="The title or name of the item")
|
||||||
description: str = Field(..., description="A descriptive explanation of the item")
|
description: str = Field(..., description="A descriptive explanation of the item")
|
||||||
@@ -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")
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
|
|
||||||
|
|
||||||
class KOQuestion(BaseModel):
|
class KOQuestion(BaseModel):
|
||||||
title: str = Field(..., description="The title of the knockout criterium.")
|
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_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.")
|
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):
|
class KOQuestions(BaseModel):
|
||||||
ko_questions: List[KOQuestion] = Field(
|
ko_questions: List[KOQuestion] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="KO Questions and answers."
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -181,8 +181,8 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
|||||||
answer = f"Let's start our selection process by asking you a few important questions."
|
answer = f"Let's start our selection process by asking you a few important questions."
|
||||||
|
|
||||||
if arguments.language != 'en':
|
if arguments.language != 'en':
|
||||||
TranslationServices.translate_config(ko_form, "fields", arguments.language)
|
TranslationServices.translate_config(self.tenant_id, ko_form, "fields", arguments.language)
|
||||||
TranslationServices.translate(answer, arguments.language)
|
TranslationServices.translate(self.tenant_id, answer, arguments.language)
|
||||||
|
|
||||||
|
|
||||||
results = SpecialistResult.create_for_type(self.type, self.type_version,
|
results = SpecialistResult.create_for_type(self.type, self.type_version,
|
||||||
@@ -234,7 +234,8 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
|||||||
if arguments.language != 'nl':
|
if arguments.language != 'nl':
|
||||||
answer = TranslationServices.translate(answer, arguments.language)
|
answer = TranslationServices.translate(answer, arguments.language)
|
||||||
if arguments.language != 'en':
|
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,
|
results = SpecialistResult.create_for_type(self.type, self.type_version,
|
||||||
answer=answer,
|
answer=answer,
|
||||||
form_request=contact_form,
|
form_request=contact_form,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -73,7 +73,7 @@ def get_public_table_names():
|
|||||||
return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage',
|
return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage',
|
||||||
'business_event_log', 'tenant_project', 'partner', 'partner_service', 'invoice', 'license_period',
|
'business_event_log', 'tenant_project', 'partner', 'partner_service', 'invoice', 'license_period',
|
||||||
'license_change_log', 'partner_service_license_tier', 'payment', 'partner_tenant', 'tenant_make',
|
'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()
|
PUBLIC_TABLES = get_public_table_names()
|
||||||
logger.info(f"Public tables: {PUBLIC_TABLES}")
|
logger.info(f"Public tables: {PUBLIC_TABLES}")
|
||||||
|
|||||||
@@ -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 ###
|
||||||
Reference in New Issue
Block a user