- Fixed Error where Catalog Types other than default could not be added

- Fixed error in TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST
- Minor improvements
This commit is contained in:
Josako
2025-07-25 22:35:08 +02:00
parent ba523a95c5
commit 42ffe3795f
13 changed files with 258 additions and 102 deletions

View File

@@ -16,7 +16,7 @@ backstory: >
AI-driven sourcing. Youre more than a recruiter—youre a trusted advisor, a brand ambassador, and a connector of AI-driven sourcing. Youre more than a recruiter—youre a trusted advisor, a brand ambassador, and a connector of
people and purpose. people and purpose.
{custom_backstory} {custom_backstory}
full_model_name: "mistral.magistral-medium-latest" full_model_name: "mistral.mistral-medium-latest"
temperature: 0.3 temperature: 0.3
metadata: metadata:
author: "Josako" author: "Josako"

View File

@@ -16,7 +16,7 @@ backstory: >
AI-driven sourcing. Youre more than a recruiter—youre a trusted advisor, a brand ambassador, and a connector of AI-driven sourcing. Youre more than a recruiter—youre a trusted advisor, a brand ambassador, and a connector of
people and purpose. people and purpose.
{custom_backstory} {custom_backstory}
full_model_name: "mistral.magistral-medium-latest" full_model_name: "mistral.mistral-medium-latest"
temperature: 0.3 temperature: 0.3
metadata: metadata:
author: "Josako" author: "Josako"

View File

@@ -8,8 +8,8 @@ task_description: >
- A short (1 sentence), closed-ended question (Yes / No) to ask the recruitment candidate. Use your experience to ask a question that - A short (1 sentence), closed-ended question (Yes / No) to ask the recruitment candidate. Use your experience to ask a question that
enables us to verify compliancy to the criterium. enables us to verify compliancy to the criterium.
- A set of 2 short answers (1 small sentence each) to that question (positive answer / negative answer), from the - A set of 2 short answers (1 small sentence of about 10 words each) to that question (positive answer / negative answer), from the
candidates perspective. candidates perspective. Do not just repeat the words already formulated in the question.
The positive answer will result in a positive evaluation of the criterium, the negative answer in a negative evaluation The positive answer will result in a positive evaluation of the criterium, the negative answer in a negative evaluation
of the criterium. Try to avoid just using Yes / No as positive and negative answers. of the criterium. Try to avoid just using Yes / No as positive and negative answers.

View File

@@ -4,7 +4,7 @@ CATALOG_TYPES = {
"name": "Standard Catalog", "name": "Standard Catalog",
"description": "A Catalog with information in Evie's Library, to be considered as a whole", "description": "A Catalog with information in Evie's Library, to be considered as a whole",
}, },
"TRAICIE_RQC": { "TRAICIE_ROLE_DEFINITION_CATALOG": {
"name": "Role Definition Catalog", "name": "Role Definition Catalog",
"description": "A Catalog with information about roles, to be considered as a whole", "description": "A Catalog with information about roles, to be considered as a whole",
"partner": "traicie" "partner": "traicie"

View File

@@ -26,7 +26,7 @@
<div class="col-md-6"> <div class="col-md-6">
<p><strong>Type Version:</strong> {{ asset.type_version }}</p> <p><strong>Type Version:</strong> {{ asset.type_version }}</p>
<p><strong>File Type:</strong> {{ asset.file_type }}</p> <p><strong>File Type:</strong> {{ asset.file_type }}</p>
<p><strong>File Size:</strong> {{ asset.file_size or 'N/A' }} bytes</p> <p><strong>File Size:</strong> {{ asset.file_size or 'N/A' }} MiB</p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field %}
{% block title %}Specialist Magic Link URLs{% endblock %}
{% block content_title %}Specialist Magic Link URLs{% endblock %}
{% block content_description %}View URL and QR Code for a Magic Link{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = [] %}
{% set exclude_fields = [] %}
{% for field in form %}
{% if field.name == 'qr_code_url' and field.data %}
<div class="form-group">
<label for="{{ field.id }}">{{ field.label.text }}</label>
<div style="max-width: 200px;">
<img src="{{ field.data }}" alt="QR Code" class="img-fluid">
</div>
<input type="hidden" name="{{ field.name }}" value="{{ field.data|e }}">
</div>
{% elif field.name == 'chat_client_url' %}
<div class="form-group">
<label for="{{ field.id }}" class="form-label">{{ field.label.text }}</label>
<div class="input-group">
<input type="text" class="form-control" value="{{ field.data }}" id="{{ field.id }}" readonly>
<a href="{{ field.data }}" class="btn btn-primary" target="_blank">Open link</a>
</div>
<input type="hidden" name="{{ field.name }}" value="{{ field.data|e }}">
</div>
{% else %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endif %}
{% endfor %}
</form>
{% endblock %}
{% block content_footer %}
{% endblock %}

View File

@@ -392,7 +392,7 @@ def add_document():
flash(f'Processing on document {new_doc.name}, version {new_doc_vers.id} started. Task ID: {task_id}.', flash(f'Processing on document {new_doc.name}, version {new_doc_vers.id} started. Task ID: {task_id}.',
'success') 'success')
return redirect(prefixed_url_for('document_bp.documents')) return redirect(prefixed_url_for('document_bp.documents_processing'))
except EveAIException as e: except EveAIException as e:
flash(str(e), 'error') flash(str(e), 'error')
@@ -451,7 +451,7 @@ def add_url():
flash(f'Processing on document {new_doc.name}, version {new_doc_vers.id} started. Task ID: {task_id}.', flash(f'Processing on document {new_doc.name}, version {new_doc_vers.id} started. Task ID: {task_id}.',
'success') 'success')
return redirect(prefixed_url_for('document_bp.documents')) return redirect(prefixed_url_for('document_bp.documents_processing'))
except EveAIException as e: except EveAIException as e:
current_app.logger.error(f"Error adding document: {str(e)}") current_app.logger.error(f"Error adding document: {str(e)}")

View File

@@ -134,8 +134,7 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
render_kw={'readonly': True}) render_kw={'readonly': True})
specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True}) specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True})
specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True}) specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
chat_client_url = StringField('Chat Client URL', validators=[Optional()], render_kw={'readonly': True})
qr_code_url = StringField('QR Code', validators=[Optional()], render_kw={'readonly': True})
tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int) tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int)
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
@@ -158,5 +157,14 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes] self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes]
class ViewSpecialistMagicLinkURLsForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)], render_kw={'readonly': True})
chat_client_url = StringField('Chat Client URL', validators=[Optional()], render_kw={'readonly': True})
qr_code_url = StringField('QR Code', validators=[Optional()], render_kw={'readonly': True})

View File

@@ -29,7 +29,7 @@ from common.utils.view_assistants import form_validation_failed, prepare_table_f
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm, from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
EditEveAIToolForm, ExecuteSpecialistForm, EditEveAIToolForm, ExecuteSpecialistForm,
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm) SpecialistMagicLinkForm, EditSpecialistMagicLinkForm, ViewSpecialistMagicLinkURLsForm)
from eveai_app.views.list_views.interaction_list_views import (get_specialists_list_view, get_assets_list_view, from eveai_app.views.list_views.interaction_list_views import (get_specialists_list_view, get_assets_list_view,
get_magic_links_list_view, get_chat_sessions_list_view, get_magic_links_list_view, get_chat_sessions_list_view,
@@ -648,52 +648,6 @@ def edit_specialist_magic_link(specialist_magic_link_id):
else: else:
form.tenant_make_id.data = specialist_ml.tenant_make_id form.tenant_make_id.data = specialist_ml.tenant_make_id
# Set the chat client URL
tenant_id = session.get('tenant').get('id')
chat_client_prefix = current_app.config.get('CHAT_CLIENT_PREFIX', 'chat_client/chat/')
base_url = request.url_root
magic_link_code = specialist_ml.magic_link_code
# Parse the URL om poortinformatie te behouden als deze afwijkt van de standaard
url_parts = request.url.split('/')
host_port = url_parts[2] # Dit bevat zowel hostname als poort indien aanwezig
# Generate the full URL for chat client with magic link code
chat_client_url = f"{request.scheme}://{host_port}/{chat_client_prefix}{magic_link_code}"
form.chat_client_url.data = chat_client_url
# Generate QR code as data URI for direct embedding in HTML
try:
import qrcode
import io
import base64
# Generate QR code as PNG for better compatibility
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4
)
qr.add_data(chat_client_url)
qr.make(fit=True)
# Generate PNG image in memory
img = qr.make_image(fill_color="black", back_color="white")
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_data = buffer.getvalue()
# Create data URI for direct embedding in HTML
img_base64 = base64.b64encode(img_data).decode('utf-8')
data_uri = f"data:image/png;base64,{img_base64}"
# Store the data URI in the form data
form.qr_code_url.data = data_uri
except Exception as e:
current_app.logger.error(f"Failed to generate QR code: {str(e)}")
form.qr_code_url.data = "Error generating QR code"
if form.validate_on_submit(): if form.validate_on_submit():
# Update the basic fields # Update the basic fields
form.populate_obj(specialist_ml) form.populate_obj(specialist_ml)
@@ -722,6 +676,61 @@ def edit_specialist_magic_link(specialist_magic_link_id):
return render_template('interaction/edit_specialist_magic_link.html', form=form) return render_template('interaction/edit_specialist_magic_link.html', form=form)
@interaction_bp.route('/view_specialist_magic_link_urls/<int:specialist_magic_link_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_specialist_magic_link_urls(specialist_magic_link_id):
specialist_ml = SpecialistMagicLink.query.get_or_404(specialist_magic_link_id)
form = ViewSpecialistMagicLinkURLsForm(obj=specialist_ml)
# Set the chat client URL
tenant_id = session.get('tenant').get('id')
chat_client_prefix = current_app.config.get('CHAT_CLIENT_PREFIX', 'chat_client/chat/')
base_url = request.url_root
magic_link_code = specialist_ml.magic_link_code
# Parse the URL om poortinformatie te behouden als deze afwijkt van de standaard
url_parts = request.url.split('/')
host_port = url_parts[2] # Dit bevat zowel hostname als poort indien aanwezig
# Generate the full URL for chat client with magic link code
chat_client_url = f"{request.scheme}://{host_port}/{chat_client_prefix}{magic_link_code}"
form.chat_client_url.data = chat_client_url
# Generate QR code as data URI for direct embedding in HTML
try:
import qrcode
import io
import base64
# Generate QR code as PNG for better compatibility
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4
)
qr.add_data(chat_client_url)
qr.make(fit=True)
# Generate PNG image in memory
img = qr.make_image(fill_color="black", back_color="white")
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_data = buffer.getvalue()
# Create data URI for direct embedding in HTML
img_base64 = base64.b64encode(img_data).decode('utf-8')
data_uri = f"data:image/png;base64,{img_base64}"
# Store the data URI in the form data
form.qr_code_url.data = data_uri
except Exception as e:
current_app.logger.error(f"Failed to generate QR code: {str(e)}")
form.qr_code_url.data = "Error generating QR code"
return render_template('interaction/view_specialist_magic_link_urls.html', form=form)
@interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST']) @interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialist_magic_links(): def specialist_magic_links():
@@ -743,6 +752,9 @@ def handle_specialist_magic_link_selection():
if action == "edit_specialist_magic_link": if action == "edit_specialist_magic_link":
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link', return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
specialist_magic_link_id=specialist_ml_id)) specialist_magic_link_id=specialist_ml_id))
if action == "view_specialist_magic_link_urls":
return redirect(prefixed_url_for('interaction_bp.view_specialist_magic_link_urls',
specialist_magic_link_id=specialist_ml_id))
return redirect(prefixed_url_for('interaction_bp.specialists')) return redirect(prefixed_url_for('interaction_bp.specialists'))

View File

@@ -132,7 +132,8 @@ def get_magic_links_list_view():
# Action definitions # Action definitions
actions = [ actions = [
{'value': 'edit_specialist_magic_link', 'text': 'Edit Magic Link', 'class': 'btn-primary', 'requiresSelection': True}, {'value': 'edit_specialist_magic_link', 'text': 'Edit Magic Link', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_specialist_magic_link', 'text': 'Create Magic Link', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False} {'value': 'view_specialist_magic_link_urls', 'text': 'View Magic Link URLs', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'create_specialist_magic_link', 'text': 'Create Magic Link', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
] ]
# Initial sort configuration # Initial sort configuration

View File

@@ -43,3 +43,11 @@ class KOQuestions(BaseModel):
"""Get the list of KOQuestion objects""" """Get the list of KOQuestion objects"""
return self.ko_questions return self.ko_questions
def get_by_title(self, title: str) -> Optional[KOQuestion]:
return next((q for q in self.ko_questions if q.title == title), None)
def get_next_by_title(self, title: str) -> Optional[KOQuestion]:
for idx, q in enumerate(self.ko_questions):
if q.title == title:
return self.ko_questions[idx + 1] if idx + 1 < len(self.ko_questions) else None
return None

View File

@@ -152,6 +152,7 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
agent_backstory = agent_config.get('backstory', '').replace('{custom_backstory}', agent.backstory or '') agent_backstory = agent_config.get('backstory', '').replace('{custom_backstory}', agent.backstory or '')
agent_backstory = self._replace_system_variables(agent_backstory) agent_backstory = self._replace_system_variables(agent_backstory)
agent_full_model_name = agent_config.get('full_model_name', 'mistral.mistral-large-latest') agent_full_model_name = agent_config.get('full_model_name', 'mistral.mistral-large-latest')
current_app.logger.debug(f"Full model name for {agent.type}: {agent_full_model_name}")
agent_temperature = agent_config.get('temperature', 0.3) agent_temperature = agent_config.get('temperature', 0.3)
llm = get_crewai_llm(agent_full_model_name, agent_temperature) llm = get_crewai_llm(agent_full_model_name, agent_temperature)
if not llm: if not llm:
@@ -331,6 +332,7 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
for state_name, result_name in self._state_result_relations.items(): for state_name, result_name in self._state_result_relations.items():
if result_name in last_interaction.specialist_results: if result_name in last_interaction.specialist_results:
setattr(self.flow.state, state_name, last_interaction.specialist_results[result_name]) setattr(self.flow.state, state_name, last_interaction.specialist_results[result_name])
#TODO: Hier wordt steeds een dict of json terug gegeven, geen pydantic model?
# Initialize the standard state values # Initialize the standard state values
self.flow.state.answer = None self.flow.state.answer = None

View File

@@ -1,4 +1,5 @@
import json import json
import random
from datetime import date from datetime import date
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@@ -33,6 +34,17 @@ KO_CRITERIA_NOT_MET_MESSAGE = ("Thank you for answering our questions! We proces
"not comply with the minimum requirements for this job. Therefor, we stop this" "not comply with the minimum requirements for this job. Therefor, we stop this"
"selection procedure") "selection procedure")
KO_CRITERIA_MET_MESSAGE = "We processed your answers with a positive result." KO_CRITERIA_MET_MESSAGE = "We processed your answers with a positive result."
KO_CRITERIA_NEXT_MESSAGES = [
"Thank you for your answer. Here's a next question.",
"Your answer fits our needs. We have yet another question to ask you.",
"Positive this far! Here's a follow-up question.",
"Great, thats just what we were hoping for. Lets continue with another question.",
"Appreciate your reply! Here's the next one.",
"Thanks for the input. Lets move on to the next question.",
"Thats exactly what we needed to hear. Here comes the next question.",
"Looks promising! Lets continue with another quick check.",
"Thanks! Here's another point we'd like to clarify."
]
RQC_MESSAGE = "You are well suited for this job." RQC_MESSAGE = "You are well suited for this job."
CONTACT_DATA_QUESTION = ("Are you willing to provide us with your contact data, so we can contact you to continue " CONTACT_DATA_QUESTION = ("Are you willing to provide us with your contact data, so we can contact you to continue "
"the selection process?") "the selection process?")
@@ -83,10 +95,9 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
def _config_state_result_relations(self): def _config_state_result_relations(self):
self._add_state_result_relation("rag_output") self._add_state_result_relation("rag_output")
self._add_state_result_relation("ko_criteria_questions") self._add_state_result_relation("ko_criteria_scores")
self._add_state_result_relation("ko_criteria_answers") self._add_state_result_relation("current_ko_criterium")
self._add_state_result_relation("competency_questions") self._add_state_result_relation("current_ko_criterium_idx")
self._add_state_result_relation("competency_scores")
self._add_state_result_relation("personal_contact_data") self._add_state_result_relation("personal_contact_data")
self._add_state_result_relation("contact_time_prefs") self._add_state_result_relation("contact_time_prefs")
@@ -175,25 +186,10 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
answer = initialisation_message answer = initialisation_message
ko_questions = self._get_ko_questions() ko_questions = self._get_ko_questions()
fields = {}
for ko_question in ko_questions.ko_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 = { current_ko_criterium = ko_questions.ko_questions[0].title
"type": "KO_CRITERIA_FORM", current_ko_criterium_idx = 0
"version": "1.0.0", ko_form = self._prepare_ko_question_form(ko_questions, current_ko_criterium, arguments.language)
"name": "Starter Questions",
"icon": "verified",
"fields": fields,
}
ko_form = TranslationServices.translate_config(self.tenant_id, ko_form, "fields", arguments.language)
rag_answer = self._check_and_execute_rag(arguments, formatted_context, citations) rag_answer = self._check_and_execute_rag(arguments, formatted_context, citations)
if rag_answer: if rag_answer:
@@ -202,6 +198,9 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
else: else:
answer = rag_answer.answer answer = rag_answer.answer
self.flow.state.current_ko_criterium = current_ko_criterium
self.flow.state.current_ko_criterium_idx = current_ko_criterium_idx
self.flow.state.ko_criteria_scores = []
self.flow.state.answer = answer self.flow.state.answer = answer
self.flow.state.phase = "ko_question_evaluation" self.flow.state.phase = "ko_question_evaluation"
self.flow.state.form_request = ko_form self.flow.state.form_request = ko_form
@@ -219,21 +218,40 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id, raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id,
"No form values returned") "No form values returned")
ko_questions = self._get_ko_questions()
# DEBUG CHECKS: Valideer het type van het resultaat
current_app.logger.debug(f"KO Questions result type: {type(ko_questions)}")
current_app.logger.debug(f"Is KOQuestions instance: {isinstance(ko_questions, KOQuestions)}")
current_app.logger.debug(f"KO Questions model dump: {ko_questions.model_dump()}")
current_app.logger.debug(
f"Number of ko_questions: {len(ko_questions.ko_questions) if hasattr(ko_questions, 'ko_questions') else 'No ko_questions attribute'}")
# Extra check: valideer elk item in de lijst
if hasattr(ko_questions, 'ko_questions') and ko_questions.ko_questions:
current_app.logger.debug(f"First question type: {type(ko_questions.ko_questions[0])}")
current_app.logger.debug(
f"First question is KOQuestion: {isinstance(ko_questions.ko_questions[0], KOQuestion)}")
current_app.logger.debug(
f"First question data: {ko_questions.ko_questions[0].model_dump() if hasattr(ko_questions.ko_questions[0], 'model_dump') else ko_questions.ko_questions[0]}")
previous_idx = self.flow.state.current_ko_criterium_idx
# Load the previous KO Questions # Load the previous KO Questions
previous_ko_questions = self._get_ko_questions().ko_questions previous_ko_question = ko_questions.ko_questions[previous_idx]
# Evaluate KO Criteria # Evaluate KO Criteria
evaluation = "positive" evaluation = "positive"
for criterium, answer in arguments.form_values.items(): criterium, answer = next(iter(arguments.form_values.items()))
for qa in previous_ko_questions: if TranslationServices.translate(self.tenant_id, previous_ko_question.answer_positive, arguments.language) != answer:
if qa.title == criterium: evaluation = "negative"
if TranslationServices.translate(self.tenant_id, qa.answer_positive, arguments.language) != answer:
evaluation = "negative"
break
if evaluation == "negative":
break
self.flow.state.ko_criteria_answers = arguments.form_values score = SelectionKOCriteriumScore(
criterium=criterium,
answer=answer,
score=1 if evaluation == "positive" else 0,
)
self.flow.state.ko_criteria_scores.append(score)
if evaluation == "negative": if evaluation == "negative":
answer = TranslationServices.translate(self.tenant_id, KO_CRITERIA_NOT_MET_MESSAGE, arguments.language) answer = TranslationServices.translate(self.tenant_id, KO_CRITERIA_NOT_MET_MESSAGE, arguments.language)
@@ -243,16 +261,35 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
results = SelectionResult.create_for_type(self.type, self.type_version) results = SelectionResult.create_for_type(self.type, self.type_version)
else: else:
answer = TranslationServices.translate(self.tenant_id, KO_CRITERIA_MET_MESSAGE, arguments.language)
rag_output = self._check_and_execute_rag(arguments, formatted_context, citations) rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
if rag_output: next_idx = previous_idx + 1
answer = f"{answer}\n\n{rag_output.answer}"
answer = (f"{answer}\n\n"
f"{TranslationServices.translate(self.tenant_id, RQC_MESSAGE, arguments.language)} "
f"{TranslationServices.translate(self.tenant_id, CONTACT_DATA_QUESTION, arguments.language)}")
self.flow.state.answer = answer if next_idx < len(ko_questions.ko_questions): # There's still a KO criterium to be evaluated
self.flow.state.phase = "personal_contact_data_preparation" next_ko_criterium = ko_questions.ko_questions[next_idx]
ko_form = self._prepare_ko_question_form(ko_questions, next_ko_criterium.title, arguments.language)
next_message = random.choice(KO_CRITERIA_NEXT_MESSAGES)
answer = TranslationServices.translate(self.tenant_id, next_message, arguments.language)
if rag_output:
answer = f"{rag_output.answer}\n\n{answer}"
self.flow.state.answer = answer
self.flow.state.form_request = ko_form
self.flow.state.current_ko_criterium = next_ko_criterium.title
self.flow.state.current_ko_criterium_idx = next_idx
self.flow.state.phase = "ko_question_evaluation"
else: # All KO Criteria have been met
answer = TranslationServices.translate(self.tenant_id, KO_CRITERIA_MET_MESSAGE, arguments.language)
rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
if rag_output:
answer = f"{answer}\n\n{rag_output.answer}"
answer = (f"{answer}\n\n"
f"{TranslationServices.translate(self.tenant_id, RQC_MESSAGE, arguments.language)} \n\n"
f"{TranslationServices.translate(self.tenant_id, CONTACT_DATA_QUESTION, arguments.language)}")
self.flow.state.answer = answer
self.flow.state.current_ko_criterium = ""
self.flow.state.current_ko_criterium_idx = None
self.flow.state.phase = "personal_contact_data_preparation"
results = SelectionResult.create_for_type(self.type, self.type_version,) results = SelectionResult.create_for_type(self.type, self.type_version,)
@@ -310,7 +347,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
self.flow.state.form_request = time_pref_form self.flow.state.form_request = time_pref_form
rqc_info = { rqc_info = {
"ko_criteria_answers": self.flow.state.ko_criteria_answers, "ko_criteria_scores": self.flow.state.ko_criteria_scores,
"personal_contact_data": self.flow.state.personal_contact_data, "personal_contact_data": self.flow.state.personal_contact_data,
} }
@@ -336,7 +373,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
self.flow.state.contact_time_prefs = arguments.form_values self.flow.state.contact_time_prefs = arguments.form_values
rqc_info = { rqc_info = {
"ko_criteria_answers": self.flow.state.ko_criteria_answers, "ko_criteria_scores": self.flow.state.ko_criteria_scores,
"personal_contact_data": self.flow.state.personal_contact_data, "personal_contact_data": self.flow.state.personal_contact_data,
"contact_time_prefs": self.flow.state.contact_time_prefs, "contact_time_prefs": self.flow.state.contact_time_prefs,
} }
@@ -476,8 +513,53 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
ko_questions_asset.object_name) ko_questions_asset.object_name)
ko_questions = KOQuestions.from_json(ko_questions_data) ko_questions = KOQuestions.from_json(ko_questions_data)
# DEBUG CHECKS: Valideer het type van ko_questions_data
current_app.logger.debug(f"KO Questions raw data type: {type(ko_questions_data)}")
current_app.logger.debug(
f"KO Questions raw data content: {ko_questions_data[:200] if isinstance(ko_questions_data, str) else 'Not a string'}")
# DEBUG CHECKS: Valideer het type van het resultaat
current_app.logger.debug(f"KO Questions result type: {type(ko_questions)}")
current_app.logger.debug(f"Is KOQuestions instance: {isinstance(ko_questions, KOQuestions)}")
current_app.logger.debug(f"KO Questions model dump: {ko_questions.model_dump()}")
current_app.logger.debug(
f"Number of ko_questions: {len(ko_questions.ko_questions) if hasattr(ko_questions, 'ko_questions') else 'No ko_questions attribute'}")
# Extra check: valideer elk item in de lijst
if hasattr(ko_questions, 'ko_questions') and ko_questions.ko_questions:
current_app.logger.debug(f"First question type: {type(ko_questions.ko_questions[0])}")
current_app.logger.debug(
f"First question is KOQuestion: {isinstance(ko_questions.ko_questions[0], KOQuestion)}")
current_app.logger.debug(
f"First question data: {ko_questions.ko_questions[0].model_dump() if hasattr(ko_questions.ko_questions[0], 'model_dump') else ko_questions.ko_questions[0]}")
return ko_questions return ko_questions
def _prepare_ko_question_form(self, ko_questions: KOQuestions, current_ko_criterium: str, language: str) \
-> Dict[str, Any]:
fields = {}
ko_question = ko_questions.get_by_title(current_ko_criterium)
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": f"Starter Question: {current_ko_criterium}",
"icon": "verified",
"fields": fields,
}
ko_form = TranslationServices.translate_config(self.tenant_id, ko_form, "fields", language)
return ko_form
class SelectionKOCriteriumScore(BaseModel): class SelectionKOCriteriumScore(BaseModel):
criterium: Optional[str] = Field(None, alias="criterium") criterium: Optional[str] = Field(None, alias="criterium")
@@ -529,7 +611,9 @@ class SelectionFlowState(EveAIFlowState):
"""Flow state for RAG specialist that automatically updates from task outputs""" """Flow state for RAG specialist that automatically updates from task outputs"""
input: Optional[SelectionInput] = None input: Optional[SelectionInput] = None
rag_output: Optional[RAGOutput] = None rag_output: Optional[RAGOutput] = None
ko_criteria_answers: Optional[Dict[str, str]] = None current_ko_criterium: Optional[str] = None
current_ko_criterium_idx: Optional[int] = None
ko_criteria_scores: Optional[List[SelectionKOCriteriumScore]] = None
personal_contact_data: Optional[PersonalContactData] = None personal_contact_data: Optional[PersonalContactData] = None
contact_time_prefs: Optional[ContactTimePreferences] = None contact_time_prefs: Optional[ContactTimePreferences] = None
citations: Optional[List[Dict[str, Any]]] = None citations: Optional[List[Dict[str, Any]]] = None
@@ -537,7 +621,7 @@ class SelectionFlowState(EveAIFlowState):
class SelectionResult(SpecialistResult): class SelectionResult(SpecialistResult):
rag_output: Optional[RAGOutput] = Field(None, alias="rag_output") rag_output: Optional[RAGOutput] = Field(None, alias="rag_output")
ko_criteria_answers: Optional[Dict[str, str]] = Field(None, alias="ko_criteria_answers") ko_criteria_scores: Optional[List[SelectionKOCriteriumScore]] = Field(None, alias="ko_criteria_scores")
personal_contact_data: Optional[PersonalContactData] = Field(None, alias="personal_contact_data") personal_contact_data: Optional[PersonalContactData] = Field(None, alias="personal_contact_data")
contact_time_prefs: Optional[ContactTimePreferences] = None contact_time_prefs: Optional[ContactTimePreferences] = None