- Add a new 'system' type to dynamic forms, first one defined = 'tenant_make'

- Add active field to Specialist model
- Improve Specialists view
- Propagate make for Role Definition Specialist to Selection Specialist (make is defined at the role level)
- Ensure a make with a given name can only be defined once
This commit is contained in:
Josako
2025-06-09 11:06:36 +02:00
parent 43ee9139d6
commit c4dcd6a0d3
15 changed files with 679 additions and 13 deletions

View File

@@ -29,6 +29,7 @@ class Specialist(db.Model):
tuning = db.Column(db.Boolean, nullable=True, default=False) tuning = db.Column(db.Boolean, nullable=True, default=False)
configuration = db.Column(JSONB, nullable=True) configuration = db.Column(JSONB, nullable=True)
arguments = db.Column(JSONB, nullable=True) arguments = db.Column(JSONB, nullable=True)
active = db.Column(db.Boolean, nullable=True, default=True)
# Relationship to retrievers through the association table # Relationship to retrievers through the association table
retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True, retrievers = db.relationship('SpecialistRetriever', backref='specialist', lazy=True,

View File

@@ -39,6 +39,7 @@ class Tenant(db.Model):
domains = db.relationship('TenantDomain', backref='tenant') domains = db.relationship('TenantDomain', backref='tenant')
licenses = db.relationship('License', back_populates='tenant') licenses = db.relationship('License', back_populates='tenant')
license_usages = db.relationship('LicenseUsage', backref='tenant') license_usages = db.relationship('LicenseUsage', backref='tenant')
makes = db.relationship('TenantMake', backref='tenant')
@property @property
def current_license(self): def current_license(self):
@@ -179,7 +180,7 @@ class TenantMake(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
name = db.Column(db.String(50), nullable=False) name = db.Column(db.String(50), nullable=False, unique=True)
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
active = db.Column(db.Boolean, nullable=False, default=True) active = db.Column(db.Boolean, nullable=False, default=True)
website = db.Column(db.String(255), nullable=True) website = db.Column(db.String(255), nullable=True)

View File

@@ -1,4 +1,4 @@
version: "1.1.0" version: "1.2.0"
name: "Traicie Role Definition Specialist" name: "Traicie Role Definition Specialist"
framework: "crewai" framework: "crewai"
partner: "traicie" partner: "traicie"

View File

@@ -0,0 +1,50 @@
version: "1.3.0"
name: "Traicie Role Definition Specialist"
framework: "crewai"
partner: "traicie"
chat: false
configuration: {}
arguments:
role_name:
name: "Role Name"
description: "The name of the role that is being processed. Will be used to create a selection specialist"
type: "str"
required: true
specialist_name:
name: "Specialist Name"
description: "The name the specialist will be called upon"
type: "str"
required: true
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
role_reference:
name: "Role Reference"
description: "A customer reference to the role"
type: "str"
required: false
vacancy_text:
name: "vacancy_text"
type: "text"
description: "The Vacancy Text"
required: true
results:
competencies:
name: "competencies"
type: "List[str, str]"
description: "List of vacancy competencies and their descriptions"
required: false
agents:
- type: "TRAICIE_HR_BP_AGENT"
version: "1.0"
tasks:
- type: "TRAICIE_GET_COMPETENCIES_TASK"
version: "1.1"
metadata:
author: "Josako"
date_added: "2025-05-27"
changes: "Added a make to be specified (a selection specialist now is created in context of a make"
description: "Assistant to create a new Vacancy based on Vacancy Text"

View File

@@ -0,0 +1,120 @@
version: "1.1.0"
name: "Traicie Selection Specialist"
framework: "crewai"
partner: "traicie"
chat: false
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_HR_BP_AGENT"
version: "1.0"
tasks:
- type: "TRAICIE_GET_COMPETENCIES_TASK"
version: "1.1"
metadata:
author: "Josako"
date_added: "2025-05-27"
changes: "Add make to the selection specialist"
description: "Assistant to create a new Vacancy based on Vacancy Text"

View File

@@ -10,7 +10,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm"> <form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm">
{{ render_selectable_table(headers=["Specialist ID", "Name", "Type"], rows=rows, selectable=True, id="specialistsTable") }} {{ render_selectable_table(headers=["Specialist ID", "Name", "Type", "Type Version", "Active"], rows=rows, selectable=True, id="specialistsTable") }}
<div class="form-group mt-3 d-flex justify-content-between"> <div class="form-group mt-3 d-flex justify-content-between">
<div> <div>
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button> <button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>

View File

@@ -3,12 +3,14 @@ from datetime import date
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField, from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
validators, ValidationError) validators, ValidationError)
from flask import current_app, request from flask import current_app, request, session
import json import json
from wtforms.fields.choices import SelectField from wtforms.fields.choices import SelectField
from wtforms.fields.datetime import DateField from wtforms.fields.datetime import DateField
from wtforms.fields.simple import ColorField from wtforms.fields.simple import ColorField
from common.models.user import TenantMake
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
@@ -300,6 +302,22 @@ class DynamicFormBase(FlaskForm):
except Exception as e: except Exception as e:
raise ValidationError(f"Invalid ordered list: {str(e)}") raise ValidationError(f"Invalid ordered list: {str(e)}")
def _get_system_field(self, system_name):
"""Get the field class and kwargs for a system field. Add system field cases as you need them."""
field_class = None
extra_classes = ''
field_kwargs = {}
match system_name:
case 'tenant_make':
field_class = SelectField
tenant_id = session.get('tenant').get('id')
makes = TenantMake.query.filter_by(tenant_id=tenant_id).all()
choices = [(make.name, make.name) for make in makes]
extra_classes = ''
field_kwargs = {'choices': choices}
return field_class, extra_classes, field_kwargs
def add_dynamic_fields(self, collection_name, config, initial_data=None): def add_dynamic_fields(self, collection_name, config, initial_data=None):
"""Add dynamic fields to the form based on the configuration. """Add dynamic fields to the form based on the configuration.
@@ -357,11 +375,12 @@ class DynamicFormBase(FlaskForm):
extra_classes = ['monospace-text', 'pattern-input'] extra_classes = ['monospace-text', 'pattern-input']
field_kwargs = {} field_kwargs = {}
elif field_type == 'ordered_list': elif field_type == 'ordered_list':
current_app.logger.debug(f"Adding ordered list field for {full_field_name}")
field_class = OrderedListField field_class = OrderedListField
extra_classes = '' extra_classes = ''
list_type = field_def.get('list_type', '') list_type = field_def.get('list_type', '')
field_kwargs = {'list_type': list_type} field_kwargs = {'list_type': list_type}
elif field_type == 'system':
field_class, extra_classes, field_kwargs = self._get_system_field(field_def.get('system_name', ''))
else: else:
extra_classes = '' extra_classes = ''
field_class = { field_class = {

View File

@@ -24,6 +24,7 @@ def get_tools():
class SpecialistForm(FlaskForm): class SpecialistForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
retrievers = QuerySelectMultipleField( retrievers = QuerySelectMultipleField(
'Retrievers', 'Retrievers',
@@ -34,7 +35,7 @@ class SpecialistForm(FlaskForm):
) )
type = SelectField('Specialist Type', validators=[DataRequired()]) type = SelectField('Specialist Type', validators=[DataRequired()])
active = BooleanField('Active', validators=[Optional()], default=True)
tuning = BooleanField('Enable Specialist Tuning', default=False) tuning = BooleanField('Enable Specialist Tuning', default=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -47,6 +48,7 @@ class SpecialistForm(FlaskForm):
class EditSpecialistForm(DynamicFormBase): class EditSpecialistForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired()]) name = StringField('Name', validators=[DataRequired()])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
active = BooleanField('Active', validators=[Optional()], default=True)
retrievers = QuerySelectMultipleField( retrievers = QuerySelectMultipleField(
'Retrievers', 'Retrievers',

View File

@@ -162,6 +162,7 @@ def specialist():
new_specialist.type = form.type.data new_specialist.type = form.type.data
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version( new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
new_specialist.type) new_specialist.type)
new_specialist.active = form.active.data
new_specialist.tuning = form.tuning.data new_specialist.tuning = form.tuning.data
set_logging_information(new_specialist, dt.now(tz.utc)) set_logging_information(new_specialist, dt.now(tz.utc))
@@ -231,6 +232,7 @@ def edit_specialist(specialist_id):
specialist.name = form.name.data specialist.name = form.name.data
specialist.description = form.description.data specialist.description = form.description.data
specialist.tuning = form.tuning.data specialist.tuning = form.tuning.data
specialist.active = form.active.data
# Update the configuration dynamic fields # Update the configuration dynamic fields
specialist.configuration = form.get_dynamic_data("configuration") specialist.configuration = form.get_dynamic_data("configuration")
@@ -297,7 +299,7 @@ def specialists():
# prepare table data # prepare table data
rows = prepare_table_for_macro(the_specialists, rows = prepare_table_for_macro(the_specialists,
[('id', ''), ('name', ''), ('type', '')]) [('id', ''), ('name', ''), ('type', ''), ('type_version', ''), ('active', ''),])
# Render the catalogs in a template # Render the catalogs in a template
return render_template('interaction/specialists.html', rows=rows, pagination=pagination) return render_template('interaction/specialists.html', rows=rows, pagination=pagination)

View File

@@ -2,10 +2,12 @@ from flask import current_app, session
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField, from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField) SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
import pytz import pytz
from flask_security import current_user from flask_security import current_user
from wtforms.widgets.core import HiddenInput
from common.models.user import TenantMake
from common.services.user import UserServices from common.services.user import UserServices
from config.type_defs.service_types import SERVICE_TYPES from config.type_defs.service_types import SERVICE_TYPES
from eveai_app.views.dynamic_form_base import DynamicFormBase from eveai_app.views.dynamic_form_base import DynamicFormBase
@@ -132,8 +134,19 @@ class EditTenantProjectForm(FlaskForm):
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()] self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
def validate_make_name(form, field):
# Controleer of een TenantMake met deze naam al bestaat
existing_make = TenantMake.query.filter_by(name=field.data).first()
# Als er een bestaande make is gevonden en we zijn niet in edit mode,
# of als we wel in edit mode zijn maar het is een ander record (andere id)
if existing_make and (not hasattr(form, 'id') or form.id.data != existing_make.id):
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.')
class TenantMakeForm(DynamicFormBase): class TenantMakeForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) id = IntegerField('ID', widget=HiddenInput())
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
active = BooleanField('Active', validators=[Optional()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])

View File

@@ -1,3 +1,4 @@
import json
import uuid import uuid
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app from flask import request, redirect, flash, render_template, Blueprint, session, current_app
@@ -627,15 +628,17 @@ def delete_tenant_project(tenant_project_id):
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_make(): def tenant_make():
form = TenantMakeForm() form = TenantMakeForm()
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
default_customisation_options = create_default_config_from_type_config(customisation_config["configuration"])
form.add_dynamic_fields("configuration", customisation_config, default_customisation_options)
if form.validate_on_submit(): if form.validate_on_submit():
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
new_tenant_make = TenantMake() new_tenant_make = TenantMake()
form.populate_obj(new_tenant_make) form.populate_obj(new_tenant_make)
new_tenant_make.tenant_id = tenant_id new_tenant_make.tenant_id = tenant_id
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION") customisation_options = form.get_dynamic_data("configuration")
new_tenant_make.chat_customisation_options = create_default_config_from_type_config( new_tenant_make.chat_customisation_options = json.dumps(customisation_options)
customisation_config["configuration"])
form.add_dynamic_fields("configuration", customisation_config, new_tenant_make.chat_customisation_options)
set_logging_information(new_tenant_make, dt.now(tz.utc)) set_logging_information(new_tenant_make, dt.now(tz.utc))
try: try:

View File

@@ -0,0 +1,198 @@
import asyncio
import json
from os import wait
from typing import Optional, List
from crewai.flow.flow import start, listen, and_
from flask import current_app
from pydantic import BaseModel, Field
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.user import Tenant
from common.models.interaction import Specialist
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
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
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"""
type: TRAICIE_ROLE_DEFINITION_SPECIALIST
type_version: 1.0
Traicie Role Definition 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_ROLE_DEFINITION_SPECIALIST"
@property
def type_version(self) -> str:
return "1.3"
def _config_task_agents(self):
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
def _config_pydantic_outputs(self):
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
def _instantiate_specialist(self):
verbose = self.tuning
role_definition_agents = [self.traicie_hr_bp_agent]
role_definition_tasks = [self.traicie_get_competencies_task]
self.role_definition_crew = EveAICrewAICrew(
self,
"Role Definition Crew",
agents=role_definition_agents,
tasks=role_definition_tasks,
verbose=verbose,
)
self.flow = RoleDefinitionFlow(
self,
self.role_definition_crew
)
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
self.log_tuning("Traicie Role Definition Specialist execution started", {})
flow_inputs = {
"vacancy_text": arguments.vacancy_text,
"role_name": arguments.role_name,
'role_reference': arguments.role_reference,
}
flow_results = self.flow.kickoff(inputs=flow_inputs)
flow_state = self.flow.state
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
if flow_state.competencies:
results.competencies = flow_state.competencies
self.create_selection_specialist(arguments, flow_state.competencies)
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
return results
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
selection_comptencies = []
for competency in competencies:
selection_competency = {
"title": competency.title,
"description": competency.description,
"assess": True,
"is_knockout": False,
}
selection_comptencies.append(selection_competency)
selection_config = {
"name": arguments.specialist_name,
"competencies": selection_comptencies,
"tone_of_voice": "Professional & Neutral",
"language_level": "Standard",
"role_reference": arguments.role_reference,
"make": arguments.make,
}
name = arguments.role_name
if len(name) > 50:
name = name[:47] + "..."
new_specialist = Specialist(
name=name,
description=f"Specialist for {arguments.role_name} role",
type="TRAICIE_SELECTION_SPECIALIST",
type_version="1.1",
tuning=False,
configuration=selection_config,
)
try:
db.session.add(new_specialist)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
raise e
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
class RoleDefinitionSpecialistInput(BaseModel):
role_name: str = Field(..., alias="role_name")
role_reference: Optional[str] = Field(..., alias="role_reference")
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
class RoleDefinitionSpecialistResult(SpecialistResult):
competencies: Optional[List[ListItem]] = None
class RoleDefFlowState(EveAIFlowState):
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
input: Optional[RoleDefinitionSpecialistInput] = None
competencies: Optional[List[ListItem]] = None
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
def __init__(self,
specialist_executor: CrewAIBaseSpecialistExecutor,
role_definitiion_crew: EveAICrewAICrew,
**kwargs):
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
self.specialist_executor = specialist_executor
self.role_definition_crew = role_definitiion_crew
self.exception_raised = False
@start()
def process_inputs(self):
return ""
@listen(process_inputs)
async def execute_role_definition (self):
inputs = self.state.input.model_dump()
try:
current_app.logger.debug("In execute_role_definition")
crew_output = await self.role_definition_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.role_definition_crew.tasks:
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
if task.name == "traicie_get_competencies_task":
# update["competencies"] = task.output.pydantic.competencies
self.state.competencies = task.output.pydantic.competencies
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
return crew_output
except Exception as e:
current_app.logger.error(f"CREW execute_role_definition 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 = RoleDefinitionSpecialistInput.model_validate(inputs)
current_app.logger.debug(f"State: {self.state}")
result = await super().kickoff_async(inputs)
return self.state

View File

@@ -0,0 +1,197 @@
import asyncio
import json
from os import wait
from typing import Optional, List
from crewai.flow.flow import start, listen, and_
from flask import current_app
from pydantic import BaseModel, Field
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.user import Tenant
from common.models.interaction import Specialist
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
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
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"""
type: TRAICIE_SELECTION_SPECIALIST
type_version: 1.0
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.0"
def _config_task_agents(self):
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
def _config_pydantic_outputs(self):
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
def _instantiate_specialist(self):
verbose = self.tuning
role_definition_agents = [self.traicie_hr_bp_agent]
role_definition_tasks = [self.traicie_get_competencies_task]
self.role_definition_crew = EveAICrewAICrew(
self,
"Role Definition Crew",
agents=role_definition_agents,
tasks=role_definition_tasks,
verbose=verbose,
)
self.flow = RoleDefinitionFlow(
self,
self.role_definition_crew
)
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
self.log_tuning("Traicie Role Definition Specialist execution started", {})
flow_inputs = {
"vacancy_text": arguments.vacancy_text,
"role_name": arguments.role_name,
'role_reference': arguments.role_reference,
}
flow_results = self.flow.kickoff(inputs=flow_inputs)
flow_state = self.flow.state
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
if flow_state.competencies:
results.competencies = flow_state.competencies
self.create_selection_specialist(arguments, flow_state.competencies)
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
return results
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
selection_comptencies = []
for competency in competencies:
selection_competency = {
"title": competency.title,
"description": competency.description,
"assess": True,
"is_knockout": False,
}
selection_comptencies.append(selection_competency)
selection_config = {
"name": arguments.specialist_name,
"competencies": selection_comptencies,
"tone_of_voice": "Professional & Neutral",
"language_level": "Standard",
"role_reference": arguments.role_reference,
}
name = arguments.role_name
if len(name) > 50:
name = name[:47] + "..."
new_specialist = Specialist(
name=name,
description=f"Specialist for {arguments.role_name} role",
type="TRAICIE_SELECTION_SPECIALIST",
type_version="1.0",
tuning=False,
configuration=selection_config,
)
try:
db.session.add(new_specialist)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
raise e
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
class RoleDefinitionSpecialistInput(BaseModel):
role_name: str = Field(..., alias="role_name")
role_reference: Optional[str] = Field(..., alias="role_reference")
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
class RoleDefinitionSpecialistResult(SpecialistResult):
competencies: Optional[List[ListItem]] = None
class RoleDefFlowState(EveAIFlowState):
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
input: Optional[RoleDefinitionSpecialistInput] = None
competencies: Optional[List[ListItem]] = None
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
def __init__(self,
specialist_executor: CrewAIBaseSpecialistExecutor,
role_definitiion_crew: EveAICrewAICrew,
**kwargs):
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
self.specialist_executor = specialist_executor
self.role_definition_crew = role_definitiion_crew
self.exception_raised = False
@start()
def process_inputs(self):
return ""
@listen(process_inputs)
async def execute_role_definition (self):
inputs = self.state.input.model_dump()
try:
current_app.logger.debug("In execute_role_definition")
crew_output = await self.role_definition_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.role_definition_crew.tasks:
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
if task.name == "traicie_get_competencies_task":
# update["competencies"] = task.output.pydantic.competencies
self.state.competencies = task.output.pydantic.competencies
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
return crew_output
except Exception as e:
current_app.logger.error(f"CREW execute_role_definition 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 = RoleDefinitionSpecialistInput.model_validate(inputs)
current_app.logger.debug(f"State: {self.state}")
result = await super().kickoff_async(inputs)
return self.state

View File

@@ -0,0 +1,31 @@
"""Make TenantMake name unique
Revision ID: f40d16a0965a
Revises: 200bda7f5251
Create Date: 2025-06-09 06:15:56.791634
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f40d16a0965a'
down_revision = '200bda7f5251'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
batch_op.create_unique_constraint(None, ['name'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
# ### end Alembic commands ###

View File

@@ -0,0 +1,29 @@
"""Add active field to Specialist
Revision ID: a179785e5362
Revises: c71facc0ce7e
Create Date: 2025-06-09 08:30:18.532600
"""
from alembic import op
import sqlalchemy as sa
import pgvector
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'a179785e5362'
down_revision = 'c71facc0ce7e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('specialist', sa.Column('active', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('specialist', 'active')
# ### end Alembic commands ###