From 81e754317a2a818308689bfb3cab0df004193fcb Mon Sep 17 00:00:00 2001 From: Josako Date: Sun, 1 Jun 2025 10:09:34 +0200 Subject: [PATCH] - smaller changes to eveai.css to ensure background of selected buttons do not get all white and to ensure that the background of fiels in editable cells do not become white in a tabulator. - The Role Definition Specialist now creates a new selection specialist upon completion --- .../interaction/specialist_services.py | 194 ++++++++++++++++- .../1.2.0.yaml | 44 ++++ .../TRAICIE_SELECTION_SPECIALIST/1.0.0.yaml | 9 +- eveai_app/views/interaction_views.py | 5 +- .../TRAICIE_ROLE_DEFINITION_SPECIALIST/1_2.py | 197 ++++++++++++++++++ nginx/static/assets/css/eveai.css | 20 +- 6 files changed, 460 insertions(+), 9 deletions(-) create mode 100644 config/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1.2.0.yaml create mode 100644 eveai_chat_workers/specialists/traicie/TRAICIE_ROLE_DEFINITION_SPECIALIST/1_2.py 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 */ +} + + +