diff --git a/common/services/utils/answer_check_services.py b/common/services/utils/answer_check_services.py
deleted file mode 100644
index 9363601..0000000
--- a/common/services/utils/answer_check_services.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from flask import current_app, session
-from langchain_core.output_parsers import StrOutputParser
-from langchain_core.prompts import ChatPromptTemplate
-from langchain_core.runnables import RunnablePassthrough
-
-from common.utils.business_event import BusinessEvent
-from common.utils.business_event_context import current_event
-from common.utils.model_utils import get_template
-from eveai_chat_workers.outputs.globals.q_a_output.q_a_output_v1_0 import QAOutput
-
-
-class AnswerCheckServices:
- @staticmethod
- def check_affirmative_answer(question: str, answer: str, language_iso: str) -> bool:
- return AnswerCheckServices._check_answer(question, answer, language_iso, "check_affirmative_answer",
- "Check Affirmative Answer")
-
- @staticmethod
- def check_additional_information(question: str, answer: str, language_iso: str) -> bool:
- return AnswerCheckServices._check_answer(question, answer, language_iso, "check_additional_information",
- "Check Additional Information")
-
- @staticmethod
- def _check_answer(question: str, answer: str, language_iso: str, template_name: str, span_name: str) -> bool:
- if language_iso.strip() == '':
- raise ValueError("Language cannot be empty")
- language = current_app.config.get('SUPPORTED_LANGUAGE_ISO639_1_LOOKUP').get(language_iso)
- if language is None:
- raise ValueError(f"Unsupported language: {language_iso}")
- if question.strip() == '':
- raise ValueError("Question cannot be empty")
- if answer.strip() == '':
- raise ValueError("Answer cannot be empty")
-
- tenant_id = session.get('tenant').get('id')
-
- if not current_event:
- with BusinessEvent('Answer Check Service', tenant_id):
- with current_event.create_span(span_name):
- return AnswerCheckServices._check_answer_logic(question, answer, language, template_name)
- else:
- with current_event.create_span('Check Affirmative Answer'):
- return AnswerCheckServices._check_answer_logic(question, answer, language, template_name)
-
- @staticmethod
- def _check_answer_logic(question: str, answer: str, language: str, template_name: str) -> bool:
- prompt_params = {
- 'question': question,
- 'answer': answer,
- 'language': language,
- }
-
- template, llm = get_template(template_name)
- check_answer_prompt = ChatPromptTemplate.from_template(template)
- setup = RunnablePassthrough()
-
- output_schema = QAOutput
- structured_llm = llm.with_structured_output(output_schema)
-
- chain = (setup | check_answer_prompt | structured_llm )
-
- raw_answer = chain.invoke(prompt_params)
- current_app.logger.debug(f"Raw answer: {raw_answer}")
-
- return raw_answer.answer
diff --git a/common/services/utils/human_answer_services.py b/common/services/utils/human_answer_services.py
new file mode 100644
index 0000000..ab1d0f2
--- /dev/null
+++ b/common/services/utils/human_answer_services.py
@@ -0,0 +1,108 @@
+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.a2q_output.q_a_output_v1_0 import A2QOutput
+from eveai_chat_workers.outputs.globals.q_a_output.q_a_output_v1_0 import QAOutput
+
+
+class HumanAnswerServices:
+ @staticmethod
+ def check_affirmative_answer(tenant_id: int, question: str, answer: str, language_iso: str) -> bool:
+ return HumanAnswerServices._check_answer(tenant_id, question, answer, language_iso, "check_affirmative_answer",
+ "Check Affirmative Answer")
+
+ @staticmethod
+ def check_additional_information(tenant_id: int, question: str, answer: str, language_iso: str) -> bool:
+ return HumanAnswerServices._check_answer(tenant_id, question, answer, language_iso,
+ "check_additional_information", "Check Additional Information")
+
+ @staticmethod
+ def get_answer_to_question(tenant_id: int, question: str, answer: str, language_iso: str) -> str:
+
+ language = HumanAnswerServices._process_arguments(question, answer, language_iso)
+ span_name = "Get Answer To Question"
+ template_name = "get_answer_to_question"
+
+ if not current_event:
+ with BusinessEvent('Answer Check Service', tenant_id):
+ with current_event.create_span(span_name):
+ return HumanAnswerServices._get_answer_to_question_logic(question, answer, language, template_name)
+ else:
+ with current_event.create_span('Check Affirmative Answer'):
+ return HumanAnswerServices._get_answer_to_question_logic(question, answer, language, template_name)
+
+ @staticmethod
+ def _check_answer(tenant_id: int, question: str, answer: str, language_iso: str, template_name: str,
+ span_name: str) -> bool:
+ language = HumanAnswerServices._process_arguments(question, answer, language_iso)
+ if not current_event:
+ with BusinessEvent('Answer Check Service', tenant_id):
+ with current_event.create_span(span_name):
+ return HumanAnswerServices._check_answer_logic(question, answer, language, template_name)
+ else:
+ with current_event.create_span(span_name):
+ return HumanAnswerServices._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
+
+ @staticmethod
+ def _get_answer_to_question_logic(question: str, answer: str, language: str, template_name: str) \
+ -> str:
+ 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 = A2QOutput
+ 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
+
+ @staticmethod
+ def _process_arguments(question, answer, language_iso: str) -> str:
+ 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")
+
+ return language
diff --git a/config/agents/globals/RAG_AGENT/1.1.0.yaml b/config/agents/globals/RAG_AGENT/1.1.0.yaml
new file mode 100644
index 0000000..ea8fefd
--- /dev/null
+++ b/config/agents/globals/RAG_AGENT/1.1.0.yaml
@@ -0,0 +1,22 @@
+version: "1.0.0"
+name: "Rag Agent"
+role: >
+ {tenant_name} Spokesperson. {custom_role}
+goal: >
+ You get questions by a human correspondent, and give answers based on a given context, taking into account the history
+ of the current conversation. {custom_goal}
+backstory: >
+ You are the primary contact for {tenant_name}. You are known by {name}, and can be addressed by this name, or you. You are
+ a very good communicator, and adapt to the style used by the human asking for information (e.g. formal or informal).
+ You always stay correct and polite, whatever happens. And you ensure no discriminating language is used.
+ You are perfectly multilingual in all known languages, and do your best to answer questions in {language}, whatever
+ language the context provided to you is in. You are participating in a conversation, not writing e.g. an email. Do not
+ include a salutation or closing greeting in your answer.
+ {custom_backstory}
+full_model_name: "mistral.mistral-small-latest"
+temperature: 0.3
+metadata:
+ author: "Josako"
+ date_added: "2025-01-08"
+ description: "An Agent that does RAG based on a user's question, RAG content & history"
+ changes: "Initial version"
diff --git a/config/agents/traicie/TRAICIE_RECRUITER_AGENT/1.0.1.yaml b/config/agents/traicie/TRAICIE_RECRUITER_AGENT/1.0.1.yaml
new file mode 100644
index 0000000..a0c8370
--- /dev/null
+++ b/config/agents/traicie/TRAICIE_RECRUITER_AGENT/1.0.1.yaml
@@ -0,0 +1,25 @@
+version: "1.0.1"
+name: "Traicie Recruiter"
+role: >
+ You are an Expert Recruiter working for {tenant_name}, known as {name}. You can be addressed as {name}
+ {custom_role}
+goal: >
+ As an expert recruiter, you identify, attract, and secure top talent by building genuine relationships, deeply
+ understanding business needs, and ensuring optimal alignment between candidate potential and organizational goals
+ , while championing diversity, culture fit, and long-term retention.
+ {custom_goal}
+backstory: >
+ You started your career in a high-pressure agency setting, where you quickly learned the art of fast-paced hiring and
+ relationship building. Over the years, you moved in-house, partnering closely with business leaders to shape
+ recruitment strategies that go beyond filling roles—you focus on finding the right people to drive growth and culture.
+ With a strong grasp of both tech and non-tech profiles, you’ve adapted to changing trends, from remote work to
+ AI-driven sourcing. You’re more than a recruiter—you’re a trusted advisor, a brand ambassador, and a connector of
+ people and purpose.
+ {custom_backstory}
+full_model_name: "mistral.magistral-medium-latest"
+temperature: 0.3
+metadata:
+ author: "Josako"
+ date_added: "2025-07-03"
+ description: "Traicie Recruiter Agent"
+ changes: "Ensure recruiter can be addressed by a name"
diff --git a/config/prompts/globals/check_additional_information/1.0.0.yaml b/config/prompts/globals/check_additional_information/1.0.0.yaml
index 08bd4a4..9976165 100644
--- a/config/prompts/globals/check_additional_information/1.0.0.yaml
+++ b/config/prompts/globals/check_additional_information/1.0.0.yaml
@@ -1,9 +1,13 @@
version: "1.0.0"
content: >
- Check if additional information or questions are available in the answer (answer in {language}), additional to the
- following question:
+ Check if additional information or questions are available in the following answer (answer in between triple
+ backquotes):
+
+ ```{answer}```
+
+ in addition to answers to the following question (in between triple backquotes):
- "{question}"
+ ```{question}```
Answer with True or False, without additional information.
llm_model: "mistral.mistral-medium-latest"
diff --git a/config/prompts/globals/check_affirmative_answer/1.0.0.yaml b/config/prompts/globals/check_affirmative_answer/1.0.0.yaml
index 88f61d4..052508f 100644
--- a/config/prompts/globals/check_affirmative_answer/1.0.0.yaml
+++ b/config/prompts/globals/check_affirmative_answer/1.0.0.yaml
@@ -1,8 +1,12 @@
version: "1.0.0"
content: >
- Determine if there is an affirmative answer on the following question in the provided answer (answer in {language}):
+ Determine if there is an affirmative answer on the following question (in between triple backquotes):
- {question}
+ ```{question}```
+
+ in the provided answer (in between triple backquotes):
+
+ ```{answer}```
Answer with True or False, without additional information.
llm_model: "mistral.mistral-medium-latest"
diff --git a/config/prompts/globals/get_answer_to_question/1.0.0.yaml b/config/prompts/globals/get_answer_to_question/1.0.0.yaml
new file mode 100644
index 0000000..f170def
--- /dev/null
+++ b/config/prompts/globals/get_answer_to_question/1.0.0.yaml
@@ -0,0 +1,16 @@
+version: "1.0.0"
+content: >
+ Provide us with the answer to the following question (in between triple backquotes) from the text provided to you:
+
+ ```{question}````
+
+ Reply in exact wordings and in the same language. If no answer can be found, reply with "No answer provided"
+
+ Text provided to you:
+ ```{answer}```
+llm_model: "mistral.mistral-medium-latest"
+metadata:
+ author: "Josako"
+ date_added: "2025-06-23"
+ description: "An assistant to check if the answer to a question is affirmative."
+ changes: "Initial version"
\ No newline at end of file
diff --git a/config/prompts/globals/translation_with_context/1.0.0.yaml b/config/prompts/globals/translation_with_context/1.0.0.yaml
index 21fd5eb..ec01348 100644
--- a/config/prompts/globals/translation_with_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_with_context/1.0.0.yaml
@@ -14,7 +14,7 @@ content: >
I only want you to return the translation. No explanation, no options. I need to be able to directly use your answer
without further interpretation. If more than one option is available, present me with the most probable one.
-llm_model: "mistral.ministral-8b-latest"
+llm_model: "mistral.mistral-medium-latest"
metadata:
author: "Josako"
date_added: "2025-06-23"
diff --git a/config/prompts/globals/translation_without_context/1.0.0.yaml b/config/prompts/globals/translation_without_context/1.0.0.yaml
index 6071261..c895031 100644
--- a/config/prompts/globals/translation_without_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_without_context/1.0.0.yaml
@@ -11,7 +11,7 @@ content: >
I only want you to return the translation. No explanation, no options. I need to be able to directly use your answer
without further interpretation. If more than one option is available, present me with the most probable one.
-llm_model: "mistral.ministral-8b-latest"
+llm_model: "mistral.mistral-medium-latest"
metadata:
author: "Josako"
date_added: "2025-06-23"
diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml
index 3467c46..219f7ea 100644
--- a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml
+++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.4.0.yaml
@@ -45,11 +45,6 @@ configuration:
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"
@@ -98,8 +93,8 @@ arguments:
name: "Interaction Mode"
type: "enum"
description: "The interaction mode the specialist will start working in."
- allowed_values: ["Job Application", "Seduction"]
- default: "Job Application"
+ allowed_values: ["orientation", "seduction"]
+ default: "orientation"
required: true
results:
competencies:
@@ -108,17 +103,13 @@ results:
description: "List of vacancy competencies and their descriptions"
required: false
agents:
- - type: "TRAICIE_RECRUITER_AGENT"
- version: "1.0"
- type: "RAG_AGENT"
- version: "1.0"
+ version: "1.1"
tasks:
- - type: "TRAICIE_KO_CRITERIA_INTERVIEW_DEFINITION_TASK"
- version: "1.0"
- type: "RAG_TASK"
- version: "1.0"
+ version: "1.1"
metadata:
author: "Josako"
- date_added: "2025-06-30"
- changes: "Add 'RAG' to the selection specialist"
+ date_added: "2025-07-03"
+ changes: "Update for a Full Virtual Assistant Experience"
description: "Assistant to assist in candidate selection"
\ No newline at end of file
diff --git a/config/tasks/globals/RAG_TASK/1.1.0.yaml b/config/tasks/globals/RAG_TASK/1.1.0.yaml
new file mode 100644
index 0000000..ad3a714
--- /dev/null
+++ b/config/tasks/globals/RAG_TASK/1.1.0.yaml
@@ -0,0 +1,23 @@
+version: "1.0.0"
+name: "RAG Task"
+task_description: >
+ Answer the question based on the following context, and taking into account the history of the discussion. Try not to
+ repeat answers already given in the recent history, unless confirmation is required or repetition is essential to
+ give a coherent answer.
+ {custom_description}
+ Use the following {language} in your communication, and cite the sources used at the end of the full conversation.
+ If the question cannot be answered using the given context, answer "I have insufficient information to answer this
+ question."
+ Context (in between triple backquotes):
+ ```{context}```
+ History (in between triple backquotes):
+ ```{history}```
+ Question (in between triple backquotes):
+ ```{question}```
+expected_output: >
+
+metadata:
+ author: "Josako"
+ date_added: "2025-01-08"
+ description: "A Task that gives RAG-based answers"
+ changes: "Initial version"
diff --git a/eveai_app/templates/interaction/view_chat_session.html b/eveai_app/templates/interaction/view_chat_session.html
index 7d3aa65..533fd7e 100644
--- a/eveai_app/templates/interaction/view_chat_session.html
+++ b/eveai_app/templates/interaction/view_chat_session.html
@@ -38,7 +38,7 @@
- {{ specialist_results.detailed_query if specialist_results and specialist_results.detailed_query else specialist_arguments.query }}
+ {{ specialist_results.detailed_question if specialist_results and specialist_results.detailed_question else specialist_arguments.question }}
diff --git a/eveai_chat_workers/outputs/globals/a2q_output/q_a_output_v1_0.py b/eveai_chat_workers/outputs/globals/a2q_output/q_a_output_v1_0.py
new file mode 100644
index 0000000..a6cf0d5
--- /dev/null
+++ b/eveai_chat_workers/outputs/globals/a2q_output/q_a_output_v1_0.py
@@ -0,0 +1,7 @@
+from typing import List, Optional
+
+from pydantic import BaseModel, Field
+
+
+class A2QOutput(BaseModel):
+ answer: str = Field(None, description="Answer to the question asked")
diff --git a/eveai_chat_workers/specialists/crewai_base_classes.py b/eveai_chat_workers/specialists/crewai_base_classes.py
index 5f1da70..974ad07 100644
--- a/eveai_chat_workers/specialists/crewai_base_classes.py
+++ b/eveai_chat_workers/specialists/crewai_base_classes.py
@@ -136,4 +136,9 @@ class EveAICrewAIFlow(Flow):
class EveAIFlowState(BaseModel):
"""Base class for all EveAI flow states"""
- pass
+ answer: Optional[str] = None
+ detailed_question: Optional[str] = None
+ question: Optional[str] = None
+ phase: Optional[str] = None
+ form_request: Optional[Dict[str, Any]] = None
+ citations: Optional[Dict[str, Any]] = None
diff --git a/eveai_chat_workers/specialists/crewai_base_specialist.py b/eveai_chat_workers/specialists/crewai_base_specialist.py
index 26ab800..b3762a0 100644
--- a/eveai_chat_workers/specialists/crewai_base_specialist.py
+++ b/eveai_chat_workers/specialists/crewai_base_specialist.py
@@ -78,8 +78,8 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
return "\n\n".join([
"\n\n".join([
f"HUMAN:\n"
- f"{interaction.specialist_results['detailed_query']}"
- if interaction.specialist_results.get('detailed_query') else "",
+ f"{interaction.specialist_results['detailed_question']}"
+ if interaction.specialist_results.get('detailed_question') else "",
f"{interaction.specialist_arguments.get('form_values')}"
if interaction.specialist_arguments.get('form_values') else "",
f"AI:\n{interaction.specialist_results['answer']}"
@@ -119,6 +119,11 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
result_name = state_name
self._state_result_relations[state_name] = result_name
+ def _config_default_state_result_relations(self):
+ for default_attribute_name in ['answer', 'detailed_question', 'form_request', 'phase', 'citations']:
+ self._add_state_result_relation(default_attribute_name)
+
+
@abstractmethod
def _config_state_result_relations(self):
"""Configure the state-result relations by adding state-result combinations. Use _add_state_result_relation()"""
@@ -278,14 +283,15 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
"all arguments": arguments.model_dump(),
})
- original_query = arguments.query
- detailed_query = self._detail_question(arguments.language, original_query)
+ original_question = arguments.question
+ detailed_question = self._detail_question(arguments.language, original_question)
modified_arguments = arguments.model_copy(update={
- "query": detailed_query,
- "original_query": original_query
+ "query": detailed_question,
+ "original_query": original_question
})
+
# Get retriever-specific arguments
retriever_arguments = modified_arguments.retriever_arguments
@@ -350,10 +356,16 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
def _update_specialist_results(self, specialist_results: SpecialistResult) -> SpecialistResult:
"""Update the specialist results with the latest state information"""
+ # Ensure default state result elements are passed
+ self._config_default_state_result_relations()
+
update_data = {}
state_dict = self.flow.state.model_dump()
+ current_app.logger.debug(f"Updating specialist results with state: {state_dict}")
for state_name, result_name in self._state_result_relations.items():
+ current_app.logger.debug(f"Try Updating {result_name} with {state_name}")
if state_name in state_dict and state_dict[state_name] is not None:
+ current_app.logger.debug(f"Updating {result_name} with {state_name} = {state_dict[state_name]}")
update_data[result_name] = state_dict[state_name]
return specialist_results.model_copy(update=update_data)
@@ -369,6 +381,13 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
if result_name in last_interaction.specialist_results:
setattr(self.flow.state, state_name, last_interaction.specialist_results[result_name])
+ # Initialize the standard state values
+ self.flow.state.answer = None
+ self.flow.state.detailed_question = None
+ self.flow.state.form_request = None
+ self.flow.state.phase = None
+ self.flow.state.citations = []
+
@abstractmethod
def execute(self, arguments: SpecialistArguments, formatted_context: str, citations: List[int]) -> SpecialistResult:
raise NotImplementedError
@@ -378,21 +397,21 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
if self.retrievers:
# Detail the incoming query
if self._cached_session.interactions:
- query = arguments.query
+ question = arguments.question
language = arguments.language
- detailed_query = self._detail_question(language, query)
+ detailed_question = self._detail_question(language, question)
else:
- detailed_query = arguments.query
+ detailed_question = arguments.question
modified_arguments = {
- "query": detailed_query,
- "original_query": arguments.query
+ "question": detailed_question,
+ "original_question": arguments.question
}
detailed_arguments = arguments.model_copy(update=modified_arguments)
formatted_context, citations = self._retrieve_context(detailed_arguments)
result = self.execute(detailed_arguments, formatted_context, citations)
modified_result = {
- "detailed_query": detailed_query,
+ "detailed_question": detailed_question,
"citations": citations,
}
intermediate_result = result.model_copy(update=modified_result)
diff --git a/eveai_chat_workers/specialists/globals/STANDARD_RAG_SPECIALIST/1_0.py b/eveai_chat_workers/specialists/globals/STANDARD_RAG_SPECIALIST/1_0.py
index 0f3640b..13a586b 100644
--- a/eveai_chat_workers/specialists/globals/STANDARD_RAG_SPECIALIST/1_0.py
+++ b/eveai_chat_workers/specialists/globals/STANDARD_RAG_SPECIALIST/1_0.py
@@ -209,7 +209,7 @@ class SpecialistExecutor(BaseSpecialistExecutor):
result = SpecialistResult.create_for_type(
self.type,
self.type_version,
- detailed_query=detailed_question,
+ detailed_question=detailed_question,
answer=raw_result.answer,
citations=[ctx.metadata.document_id for ctx in unique_contexts
if ctx.id in raw_result.citations],
diff --git a/eveai_chat_workers/specialists/specialist_typing.py b/eveai_chat_workers/specialists/specialist_typing.py
index 9962537..b337194 100644
--- a/eveai_chat_workers/specialists/specialist_typing.py
+++ b/eveai_chat_workers/specialists/specialist_typing.py
@@ -103,7 +103,7 @@ class SpecialistResult(BaseModel):
# Structural optional fields available for all specialists
answer: Optional[str] = Field(None, description="Optional textual answer from the specialist")
- detailed_query: Optional[str] = Field(None, description="Optional detailed query for the specialist")
+ detailed_question: Optional[str] = Field(None, description="Optional detailed question for the specialist")
form_request: Optional[Dict[str, Any]] = Field(None, description="Optional form definition to request user input")
phase: Optional[str] = Field(None, description="Phase of the specialist's workflow")
citations: Optional[Dict[str, Any]] = Field(None, description="Citations for the specialist's answer")
diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py b/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py
index 1ba7a45..e989be7 100644
--- a/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py
+++ b/eveai_chat_workers/specialists/traicie/TRAICIE_KO_INTERVIEW_DEFINITION_SPECIALIST/1_0.py
@@ -131,6 +131,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
f"corresponding to CEFR level {selected_language_level['cefr_level']}")
flow_inputs = {
+ 'name': "Evie",
'tone_of_voice': tone_of_voice,
'tone_of_voice_context': tone_of_voice_context,
'language_level': language_level,
@@ -243,6 +244,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
class KODefInput(BaseModel):
+ name: Optional[str] = Field(None, alias="name")
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")
diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_3.py b/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_3.py
index 3a973f3..a507ea1 100644
--- a/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_3.py
+++ b/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_3.py
@@ -19,7 +19,7 @@ from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew,
from common.services.interaction.specialist_services import SpecialistServices
NEW_SPECIALIST_TYPE = "TRAICIE_SELECTION_SPECIALIST"
-NEW_SPECIALIST_TYPE_VERSION = "1.3"
+NEW_SPECIALIST_TYPE_VERSION = "1.4"
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py
index de868f7..39333e7 100644
--- a/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py
+++ b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_4.py
@@ -1,29 +1,50 @@
-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 typing import Optional, List, Dict, Any
+
+from crewai.flow.flow import start, listen
from flask import current_app
from pydantic import BaseModel, Field, EmailStr
-from sqlalchemy.exc import SQLAlchemyError
-from common.extensions import db
+from common.extensions import cache_manager, db, minio_client
+from common.models.interaction import EveAIAsset
from common.models.user import Tenant
-from common.models.interaction import Specialist
+from common.services.utils.human_answer_services import HumanAnswerServices
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 common.utils.eveai_exceptions import EveAISpecialistExecutionError
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
+from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
+from eveai_chat_workers.outputs.globals.rag.rag_v1_0 import RAGOutput
+from eveai_chat_workers.outputs.traicie.knockout_questions.knockout_questions_v1_0 import KOQuestion, KOQuestions
+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
+
+INITIALISATION_MESSAGE = "Let's start the selection process by asking you a few important questions."
+START_SELECTION_QUESTION = "Do you want to start the selection procedure?"
+INSUFFICIENT_INFORMATION_MESSAGE = (
+ "We do not have the necessary information to provide you with the requested answers. "
+ "Please accept our apologies. You can ask other questions or proceed with the "
+ "selection process.")
+KO_CRITERIA_NOT_MET_MESSAGE = ("Thank you for answering our questions! We processed your answers. Unfortunately, you do"
+ "not comply with the minimum requirements for this job. Therefor, we stop this"
+ "selection procedure")
+KO_CRITERIA_MET_MESSAGE = "We processed your answers with a positive result."
+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 "
+ "the selection process?")
+NO_CONTACT_DATA_QUESTION = ("We are sorry to hear that. The only way to proceed with the selection process is "
+ "to provide us with your contact data. Do you want to provide us with your contact data?"
+ "if not, we thank you, and we'll end the selection process.")
+CONTACT_DATA_PROCESSED_MESSAGE = "We successfully processed your contact data."
+CONTACT_TIME_QUESTION = "When do you prefer us to contact you? Provide us with some preferred weekdays and times!"
+NO_CONTACT_TIME_MESSAGE = ("We could not process your preferred contact time. Can you please provide us with your "
+ "preferred contact time?")
+CONTACT_TIME_PROCESSED_MESSAGE = ("We successfully processed your preferred contact time. We will contact you as soon "
+ "as possible.")
+NO_FURTHER_QUESTIONS_MESSAGE = "We do not process further questions."
+SUCCESSFUL_ENDING_MESSAGE = "Thank you for your application. We will contact you as soon as possible!"
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
@@ -47,37 +68,39 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
@property
def type_version(self) -> str:
- return "1.3"
+ return "1.4"
def _config_task_agents(self):
- self._add_task_agent("traicie_ko_criteria_interview_definition_task", "traicie_recruiter_agent")
+ self._add_task_agent("rag_task", "rag_agent")
def _config_pydantic_outputs(self):
- self._add_pydantic_output("traicie_ko_criteria_interview_definition_task", KOQuestions, "ko_questions")
+ self._add_pydantic_output("rag_task", RAGOutput, "rag_output")
def _config_state_result_relations(self):
+ 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("competency_questions")
self._add_state_result_relation("competency_scores")
self._add_state_result_relation("personal_contact_data")
+ self._add_state_result_relation("contact_time")
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(
+ rag_agents = [self.rag_agent]
+ rag_tasks = [self.rag_task]
+ self.rag_crew = EveAICrewAICrew(
self,
- "KO Criteria Interview Definition Crew",
- agents=ko_def_agents,
- tasks=ko_def_tasks,
+ "Rag Crew",
+ agents=rag_agents,
+ tasks=rag_tasks,
verbose=verbose,
)
self.flow = SelectionFlow(
self,
- self.ko_def_crew
+ self.rag_crew,
)
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
@@ -94,73 +117,62 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
specialist_phase = self._cached_session.interactions[-1].specialist_results.get('phase', 'initial')
results = None
-
+ current_app.logger.debug(f"Specialist phase: {specialist_phase}")
match specialist_phase:
case "initial":
results = self.execute_initial_state(arguments, formatted_context, citations)
+ case "start_selection_procedure":
+ results = self.execute_start_selection_procedure_state(arguments, formatted_context, citations)
+ case "rag":
+ results = self.execute_rag_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 "personal_contact_data_preparation":
+ results = self.execute_personal_contact_data_preparation(arguments, formatted_context, citations)
+ case "personal_contact_data_processing":
+ results = self.execute_personal_contact_data_processing(arguments, formatted_context, citations)
+ case "contact_time_evaluation":
+ results = self.execute_contact_time_evaluation_state(arguments, formatted_context, citations)
case "no_valid_candidate":
- results = self.execute_no_valid_candidate(arguments, formatted_context, citations)
+ results = self.execute_no_valid_candidate_state(arguments, formatted_context, citations)
case "candidate_selected":
- results = self.execute_candidate_selected(arguments, formatted_context, citations)
+ results = self.execute_candidate_selected_state(arguments, formatted_context, citations)
- self.log_tuning(f"Traicie Selection Specialist execution ended", {"Results": results.model_dump() if results else "No info"})
+ 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", [])}")
+ interaction_mode = arguments.interaction_mode
+ if not interaction_mode:
+ interaction_mode = "selection"
+ current_app.logger.debug(f"Interaction mode: {interaction_mode}")
- 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"]})
+ welcome_message = self.specialist.configuration.get("welcome_message", "Welcome to our selection process.")
+ welcome_message = TranslationServices.translate(self.tenant_id, welcome_message, arguments.language)
- 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"]}"
+ if interaction_mode == "selection":
+ return self.execute_start_selection_procedure_state(arguments, formatted_context, citations,
+ welcome_message)
+ else: # We are in orientation mode, so we perform standard rag
+ return self.execute_rag_state(arguments, formatted_context, citations, welcome_message)
- 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']}")
+ def execute_start_selection_procedure_state(self, arguments: SpecialistArguments, formatted_context, citations,
+ start_message=None) -> SpecialistResult:
- 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}")
+ answer = ""
+ if start_message:
+ initialisation_message = TranslationServices.translate(self.tenant_id, INITIALISATION_MESSAGE,
+ arguments.language)
+ answer = f"{start_message}\n\n{initialisation_message}"
+ ko_questions = self._get_ko_questions()
fields = {}
- for ko_question in self.flow.state.ko_criteria_questions:
+ for ko_question in ko_questions.ko_questions:
+ current_app.logger.debug(f"KO Question: {ko_question}")
fields[ko_question.title] = {
"name": ko_question.title,
"description": ko_question.title,
@@ -178,105 +190,259 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"fields": fields,
}
- answer = f"Let's start our selection process by asking you a few important questions."
+ rag_answer = self._check_and_execute_rag(arguments, formatted_context, citations)
+ if rag_answer:
+ if answer:
+ answer = f"{answer}\n\n{rag_answer.answer}"
+ else:
+ answer = rag_answer.answer
- if arguments.language != 'en':
- TranslationServices.translate_config(self.tenant_id, ko_form, "fields", arguments.language)
- TranslationServices.translate(self.tenant_id, answer, arguments.language)
+ self.flow.state.answer = answer
+ self.flow.state.phase = "ko_question_evaluation"
+ self.flow.state.form_request = ko_form
-
- results = SpecialistResult.create_for_type(self.type, self.type_version,
- answer=answer,
- form_request=ko_form,
- phase="ko_question_evaluation")
+ results = SelectionResult.create_for_type(self.type, self.type_version)
return results
- def execute_ko_question_evaluation(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
+ 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")
+ 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
+ previous_ko_questions = self._get_ko_questions().ko_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:
+ if qa.title == criterium:
+ if qa.answer_positive != answer:
evaluation = "negative"
break
if evaluation == "negative":
break
+ self.flow.state.ko_criteria_answers = arguments.form_values
+
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)
+ answer = TranslationServices.translate(self.tenant_id, KO_CRITERIA_NOT_MET_MESSAGE, arguments.language)
- results = SpecialistResult.create_for_type(self.type, self.type_version,
- answer=answer,
- form_request=None,
- phase="no_valid_candidate")
+ self.flow.state.answer = answer
+ self.flow.state.phase = "no_valid_candidate"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version)
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
+ 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)} "
+ f"{TranslationServices.translate(self.tenant_id, CONTACT_DATA_QUESTION, arguments.language)}")
+
+ self.flow.state.answer = answer
+ self.flow.state.phase = "personal_contact_data_preparation"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
+
+ return results
+
+ def execute_personal_contact_data_preparation(self, arguments: SpecialistArguments, formatted_context, citations) \
+ -> SpecialistResult:
+ self.log_tuning("Traicie Selection Specialist personal_contact_data_preparation started", {})
+
+ if HumanAnswerServices.check_affirmative_answer(self.tenant_id, CONTACT_DATA_QUESTION,
+ arguments.question, arguments.language):
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")
+ contact_form = TranslationServices.translate_config(self.tenant_id, contact_form, "fields",
+ arguments.language)
+ rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
+ if rag_output:
+ answer = f"{rag_output.answer}"
+ else:
+ answer = ""
+
+ self.flow.state.answer = answer
+ self.flow.state.form_request = contact_form
+ self.flow.state.phase = "personal_contact_data_processing"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
+ else:
+ answer = TranslationServices.translate(self.tenant_id, NO_CONTACT_DATA_QUESTION, arguments.language)
+
+ self.flow.state.answer = answer
+ self.flow.state.phase = "personal_contact_data_preparation"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
return results
- def execute_personal_contact_data(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
- self.log_tuning("Traicie Selection Specialist personal_contact_data started", {})
+ def execute_personal_contact_data_processing(self, arguments: SpecialistArguments, formatted_context, citations) \
+ -> SpecialistResult:
+ self.log_tuning("Traicie Selection Specialist personal_contact_data_processing started", {})
+ answer = (
+ f"{TranslationServices.translate(self.tenant_id, CONTACT_DATA_PROCESSED_MESSAGE, arguments.language)}\n"
+ f"{TranslationServices.translate(self.tenant_id, CONTACT_TIME_QUESTION, arguments.language)}")
- 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")
+ rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
+ if rag_output:
+ answer = f"{answer}\n\n{rag_output.answer}"
+
+ self.flow.state.answer = answer
+ self.flow.state.phase = "contact_time_evaluation"
+ self.flow.state.personal_contact_data = arguments.form_values
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
return results
- def execute_no_valid_candidate(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
+ def execute_contact_time_evaluation_state(self, arguments: SpecialistArguments, formatted_context, citations) \
+ -> SpecialistResult:
+ self.log_tuning("Traicie Selection Specialist contact_time_evaluation started", {})
+ contact_time_answer = HumanAnswerServices.get_answer_to_question(self.tenant_id, CONTACT_TIME_QUESTION,
+ arguments.question, arguments.language)
+
+ rag_output = self._check_and_execute_rag(arguments, formatted_context, citations)
+ if contact_time_answer == "No answer provided":
+ answer = TranslationServices.translate(self.tenant_id, NO_CONTACT_TIME_MESSAGE, arguments.language)
+ if rag_output:
+ answer = f"{answer}\n\n{rag_output.answer}"
+
+ self.flow.state.answer = answer
+ self.flow.state.phase = "contact_time_evaluation"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
+ else:
+ answer = TranslationServices.translate(self.tenant_id, CONTACT_TIME_PROCESSED_MESSAGE, arguments.language)
+ if rag_output:
+ answer = f"{answer}\n\n{rag_output.answer}"
+
+ self.flow.state.answer = answer
+ self.flow.state.phase = "candidate_selected"
+ self.flow.state.contact_time = contact_time_answer
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
+
+ return results
+
+ def execute_no_valid_candidate_state(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")
+ answer = (f"{TranslationServices.translate(self.tenant_id, KO_CRITERIA_NOT_MET_MESSAGE, arguments.language)}\n"
+ f"{TranslationServices.translate(self.tenant_id, NO_FURTHER_QUESTIONS_MESSAGE, arguments.language)}\n")
- 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")
+ self.flow.state.answer = answer
+ self.flow.state.phase = "no_valid_candidate"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
return results
+ def execute_candidate_selected_state(self, arguments: SpecialistArguments, formatted_context, citations) \
+ -> SpecialistResult:
+ self.log_tuning("Traicie Selection Specialist candidate_selected started", {})
+ answer = TranslationServices.translate(self.tenant_id, SUCCESSFUL_ENDING_MESSAGE, arguments.language)
-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")
+ self.flow.state.answer = answer
+ self.flow.state.phase = "candidate_selected"
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
+ return results
+
+ def execute_rag_state(self, arguments: SpecialistArguments, formatted_context, citations, welcome_message=None) \
+ -> SpecialistResult:
+ self.log_tuning("Traicie Selection Specialist rag_state started", {})
+
+ start_selection_question = TranslationServices.translate(self.tenant_id, START_SELECTION_QUESTION,
+ arguments.language)
+ if welcome_message:
+ answer = f"{welcome_message}\n\n{start_selection_question}"
+ else:
+ answer = ""
+
+ rag_results = None
+ if arguments.question:
+ if HumanAnswerServices.check_additional_information(self.tenant_id,
+ START_SELECTION_QUESTION,
+ arguments.question,
+ arguments.language):
+ rag_results = self.execute_rag(arguments, formatted_context, citations)
+ self.flow.state.rag_output = rag_results.rag_output
+ answer = f"{answer}\n{rag_results.answer}"
+
+ if HumanAnswerServices.check_affirmative_answer(self.tenant_id,
+ START_SELECTION_QUESTION,
+ arguments.question,
+ arguments.language):
+ return self.execute_start_selection_procedure_state(arguments, formatted_context, citations, answer)
+
+ self.flow.state.answer = answer
+ self.flow.state.phase = "rag"
+ self.flow.state.form_request = None
+
+ results = SelectionResult.create_for_type(self.type, self.type_version,)
+ return results
+
+ def execute_rag(self, arguments: SpecialistArguments, formatted_context, citations) -> RAGOutput:
+ self.log_tuning("RAG Specialist execution started", {})
+
+ insufficient_info_message = TranslationServices.translate(self.tenant_id,
+ INSUFFICIENT_INFORMATION_MESSAGE,
+ arguments.language)
+ if formatted_context:
+ flow_inputs = {
+ "language": arguments.language,
+ "question": arguments.question,
+ "context": formatted_context,
+ "history": self.formatted_history,
+ "name": self.specialist.configuration.get('name', ''),
+ }
+ rag_output = self.flow.kickoff(inputs=flow_inputs)
+ if rag_output.rag_output.insufficient_info:
+ rag_output.rag_output.answer = insufficient_info_message
+ else:
+ rag_output = RAGOutput(answer=insufficient_info_message,
+ insufficient_info=True)
+
+ self.log_tuning(f"RAG Specialist execution ended", {"Results": rag_output.model_dump()})
+
+ return rag_output
+
+ def _check_and_execute_rag(self, arguments: SpecialistArguments, formatted_context, citations) -> RAGOutput:
+ if HumanAnswerServices.check_additional_information(self.tenant_id,
+ START_SELECTION_QUESTION,
+ arguments.question,
+ arguments.language):
+ results = self.execute_rag(arguments, formatted_context, citations)
+ return results
+ else:
+ return None
+
+ def _get_ko_questions(self) -> KOQuestions:
+ ko_questions_asset = db.session.query(EveAIAsset).filter(
+ EveAIAsset.type == "TRAICIE_KO_CRITERIA_QUESTIONS",
+ 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) == self.specialist_id
+ ).first()
+
+ if not ko_questions_asset:
+ raise EveAISpecialistExecutionError(self.tenant_id, self.specialist_id, self.session_id,
+ "No KO criteria questions found")
+
+ ko_questions_data = minio_client.download_asset_file(self.tenant_id, ko_questions_asset.bucket_name,
+ ko_questions_asset.object_name)
+ ko_questions = KOQuestions.from_json(ko_questions_data)
+ current_app.logger.debug(f"KO Questions: {ko_questions}")
+
+ return ko_questions
class SelectionKOCriteriumScore(BaseModel):
@@ -285,12 +451,6 @@ class SelectionKOCriteriumScore(BaseModel):
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")
@@ -302,34 +462,51 @@ class PersonalContactData(BaseModel):
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 SelectionInput(BaseModel):
+ # RAG elements
+ language: Optional[str] = Field(None, alias="language")
+ question: Optional[str] = Field(None, alias="query")
+ context: Optional[str] = Field(None, alias="context")
+ citations: Optional[List[int]] = Field(None, alias="citations")
+ history: Optional[str] = Field(None, alias="history")
+ name: Optional[str] = Field(None, alias="name")
+ # Selection elements
+ region: str = Field(..., alias="region")
+ working_schedule: Optional[str] = Field(..., alias="working_schedule")
+ start_date: Optional[date] = Field(None, alias="vacancy_text")
+ 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")
+ field_values: Optional[Dict[str, Any]] = Field(None, alias="field_values")
class SelectionFlowState(EveAIFlowState):
- """Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
+ """Flow state for RAG 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")
+ rag_output: Optional[RAGOutput] = None
+ ko_criteria_answers: Optional[Dict[str, str]] = None
+ personal_contact_data: Optional[PersonalContactData] = None
+ contact_time: Optional[str] = None
+
+
+class SelectionResult(SpecialistResult):
+ rag_output: Optional[RAGOutput] = Field(None, alias="rag_output")
+ ko_criteria_answers: Optional[Dict[str, str]] = Field(None, alias="ko_criteria_answers")
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")
+ contact_time: Optional[str] = None
class SelectionFlow(EveAICrewAIFlow[SelectionFlowState]):
def __init__(self,
specialist_executor: CrewAIBaseSpecialistExecutor,
- ko_def_crew: EveAICrewAICrew,
+ rag_crew: EveAICrewAICrew,
**kwargs):
- super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
+ super().__init__(specialist_executor, "Selection Specialist Flow", **kwargs)
self.specialist_executor = specialist_executor
- self.ko_def_crew = ko_def_crew
+ self.rag_crew = rag_crew
self.exception_raised = False
@start()
@@ -337,34 +514,24 @@ class SelectionFlow(EveAICrewAIFlow[SelectionFlowState]):
return ""
@listen(process_inputs)
- async def execute_ko_def_definition(self):
+ async def execute_rag(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()}")
+ crew_output = await self.rag_crew.kickoff_async(inputs=inputs)
+ self.specialist_executor.log_tuning("RAG Crew Output", crew_output.model_dump())
+ output_pydantic = crew_output.pydantic
+ if not output_pydantic:
+ raw_json = json.loads(crew_output.raw)
+ output_pydantic = RAGOutput.model_validate(raw_json)
+ self.state.rag_output = output_pydantic
return crew_output
except Exception as e:
- current_app.logger.error(f"CREW execute_ko_def Kickoff Error: {str(e)}")
+ current_app.logger.error(f"CREW rag_crew 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