diff --git a/common/services/interaction/specialist_services.py b/common/services/interaction/specialist_services.py index 8bc0a10..d81aee5 100644 --- a/common/services/interaction/specialist_services.py +++ b/common/services/interaction/specialist_services.py @@ -1,7 +1,15 @@ import uuid -from typing import Dict, Any, Tuple +from datetime import datetime as dt, timezone as tz +from typing import Dict, Any, Tuple, Optional +from flask import current_app +from sqlalchemy.exc import SQLAlchemyError +from common.extensions import db, cache_manager +from common.models.interaction import ( + Specialist, EveAIAgent, EveAITask, EveAITool +) from common.utils.celery_utils import current_celery +from common.utils.model_logging_utils import set_logging_information, update_logging_information class SpecialistServices: @@ -27,4 +35,188 @@ class SpecialistServices: 'status': 'queued', } + @staticmethod + def initialize_specialist(specialist_id: int, specialist_type: str, specialist_version: str): + """ + Initialize an agentic specialist by creating all its components based on configuration. + Args: + specialist_id: ID of the specialist to initialize + specialist_type: Type of the specialist + specialist_version: Version of the specialist type to use + + Raises: + ValueError: If specialist not found or invalid configuration + SQLAlchemyError: If database operations fail + """ + config = cache_manager.specialists_config_cache.get_config(specialist_type, specialist_version) + if not config: + raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}") + if config['framework'] == 'langchain': + pass # Langchain does not require additional items to be initialized. All configuration is in the specialist. + + specialist = Specialist.query.get(specialist_id) + if not specialist: + raise ValueError(f"Specialist with ID {specialist_id} not found") + + if config['framework'] == 'crewai': + SpecialistServices.initialize_crewai_specialist(specialist, config) + + @staticmethod + def initialize_crewai_specialist(specialist: Specialist, config: Dict[str, Any]): + timestamp = dt.now(tz=tz.utc) + + try: + # Initialize agents + if 'agents' in config: + for agent_config in config['agents']: + SpecialistServices._create_agent( + specialist_id=specialist.id, + agent_type=agent_config['type'], + agent_version=agent_config['version'], + name=agent_config.get('name'), + description=agent_config.get('description'), + timestamp=timestamp + ) + + # Initialize tasks + if 'tasks' in config: + for task_config in config['tasks']: + SpecialistServices._create_task( + specialist_id=specialist.id, + task_type=task_config['type'], + task_version=task_config['version'], + name=task_config.get('name'), + description=task_config.get('description'), + timestamp=timestamp + ) + + # Initialize tools + if 'tools' in config: + for tool_config in config['tools']: + SpecialistServices._create_tool( + specialist_id=specialist.id, + tool_type=tool_config['type'], + tool_version=tool_config['version'], + name=tool_config.get('name'), + description=tool_config.get('description'), + timestamp=timestamp + ) + + db.session.commit() + current_app.logger.info(f"Successfully initialized crewai specialist {specialist.id}") + + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error(f"Database error initializing crewai specialist {specialist.id}: {str(e)}") + raise + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Error initializing crewai specialist {specialist.id}: {str(e)}") + raise + + @staticmethod + def _create_agent( + specialist_id: int, + agent_type: str, + agent_version: str, + name: Optional[str] = None, + description: Optional[str] = None, + timestamp: Optional[dt] = None + ) -> EveAIAgent: + """Create an agent with the given configuration.""" + if timestamp is None: + timestamp = dt.now(tz=tz.utc) + + # Get agent configuration from cache + agent_config = cache_manager.agents_config_cache.get_config(agent_type, agent_version) + + agent = EveAIAgent( + specialist_id=specialist_id, + name=name or agent_config.get('name', agent_type), + description=description or agent_config.get('metadata').get('description', ''), + type=agent_type, + type_version=agent_version, + role=None, + goal=None, + backstory=None, + tuning=False, + configuration=None, + arguments=None + ) + + set_logging_information(agent, timestamp) + + db.session.add(agent) + current_app.logger.info(f"Created agent {agent.id} of type {agent_type}") + return agent + + @staticmethod + def _create_task( + specialist_id: int, + task_type: str, + task_version: str, + name: Optional[str] = None, + description: Optional[str] = None, + timestamp: Optional[dt] = None + ) -> EveAITask: + """Create a task with the given configuration.""" + if timestamp is None: + timestamp = dt.now(tz=tz.utc) + + # Get task configuration from cache + task_config = cache_manager.tasks_config_cache.get_config(task_type, task_version) + + task = EveAITask( + specialist_id=specialist_id, + name=name or task_config.get('name', task_type), + description=description or task_config.get('metadata').get('description', ''), + type=task_type, + type_version=task_version, + task_description=None, + expected_output=None, + tuning=False, + configuration=None, + arguments=None, + context=None, + asynchronous=False, + ) + + set_logging_information(task, timestamp) + + db.session.add(task) + current_app.logger.info(f"Created task {task.id} of type {task_type}") + return task + + @staticmethod + def _create_tool( + specialist_id: int, + tool_type: str, + tool_version: str, + name: Optional[str] = None, + description: Optional[str] = None, + timestamp: Optional[dt] = None + ) -> EveAITool: + """Create a tool with the given configuration.""" + if timestamp is None: + timestamp = dt.now(tz=tz.utc) + + # Get tool configuration from cache + tool_config = cache_manager.tools_config_cache.get_config(tool_type, tool_version) + + tool = EveAITool( + specialist_id=specialist_id, + name=name or tool_config.get('name', tool_type), + description=description or tool_config.get('metadata').get('description', ''), + type=tool_type, + type_version=tool_version, + tuning=False, + configuration=None, + arguments=None, + ) + + set_logging_information(tool, timestamp) + + db.session.add(tool) + current_app.logger.info(f"Created tool {tool.id} of type {tool_type}") + return tool diff --git a/config/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1.2.0.yaml b/config/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1.2.0.yaml new file mode 100644 index 0000000..385a676 --- /dev/null +++ b/config/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1.2.0.yaml @@ -0,0 +1,44 @@ +version: "1.1.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 + 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: "Updated for unified competencies and ko criteria" + description: "Assistant to create a new Vacancy based on Vacancy Text" \ No newline at end of file diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml index 6c4d323..1a14925 100644 --- a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml @@ -5,12 +5,17 @@ partner: "traicie" chat: false configuration: name: - name: "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 competencies: - name: "competencies" + name: "Competencies" description: "An ordered list of competencies." type: "ordered_list" list_type: "competency_details" diff --git a/eveai_app/views/interaction_views.py b/eveai_app/views/interaction_views.py index be00f03..7503210 100644 --- a/eveai_app/views/interaction_views.py +++ b/eveai_app/views/interaction_views.py @@ -24,9 +24,6 @@ from common.utils.model_logging_utils import set_logging_information, update_log from common.utils.middleware import mw_before_request from common.utils.nginx_utils import prefixed_url_for from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro -from common.utils.specialist_utils import initialize_specialist - -from config.type_defs.specialist_types import SPECIALIST_TYPES from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm, EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm) @@ -184,7 +181,7 @@ def specialist(): current_app.logger.info(f'Specialist {new_specialist.name} successfully added for tenant {tenant_id}!') # Initialize the newly create specialist - initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version) + SpecialistServices.initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version) return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=new_specialist.id)) diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_2.py b/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_2.py new file mode 100644 index 0000000..4f2a8e6 --- /dev/null +++ b/eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_2.py @@ -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_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.1" + + 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 diff --git a/nginx/static/assets/css/eveai.css b/nginx/static/assets/css/eveai.css index 02fb058..d2c859d 100644 --- a/nginx/static/assets/css/eveai.css +++ b/nginx/static/assets/css/eveai.css @@ -398,8 +398,8 @@ input[type="radio"] { } .btn-danger:hover { - background-color: darken(var(--bs-danger), 10%) !important; /* Darken the background on hover */ - border-color: darken(var(--bs-danger), 10%) !important; /* Darken the border on hover */ + background-color: var(--bs-secondary) !important; + border-color: var(--bs-secondary) !important; color: var(--bs-white) !important; /* Ensure the text remains white and readable */ } @@ -1178,3 +1178,19 @@ select.select2[multiple] { box-shadow: 0 4px 8px rgba(118, 89, 154, 0.2); /* Consistent shadow */ } +/* Tekst in invoervelden zwart maken voor betere leesbaarheid */ +.ordered-list-editor .tabulator-row:hover .tabulator-cell input, +.ordered-list-editor .tabulator-row:hover .tabulator-cell select, +.ordered-list-editor .tabulator-row:hover .tabulator-cell textarea, +.ordered-list-editor .tabulator-row:hover .tabulator-cell .tabulator-editor, +.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell input, +.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell select, +.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell textarea, +.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell .tabulator-editor { + color: #000000 !important; /* Zwarte tekst op witte achtergrond */ + background-color: #ffffff !important; /* Witte achtergrond verzekeren */ + border: 1px solid var(--bs-primary) !important; /* Duidelijke rand toevoegen */ +} + + +