- Move global config files to globals iso global folder, as the name global conflicts with python language

- Creation of Traicie Vancancy Definition specialist
- Allow to invoke non-interaction specialists from withing Evie's mgmt interface (eveai_app)
- Improvements to crewai specialized classes
- Introduction to json editor for showing specialists arguments and results in a better way
- Introduction of more complex pagination (adding extra arguments) by adding a global 'get_pagination_html'
- Allow follow-up of ChatSession / Specialist execution
- Improvement in logging of Specialists (but needs to be finished)
This commit is contained in:
Josako
2025-05-26 11:26:03 +02:00
parent d789e431ca
commit 1fdbd2ff45
94 changed files with 1657 additions and 443 deletions

View File

@@ -8,7 +8,7 @@ from .document import Embedding, Retriever
class ChatSession(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True)
session_id = db.Column(db.String(36), nullable=True)
session_id = db.Column(db.String(49), nullable=True)
session_start = db.Column(db.DateTime, nullable=False)
session_end = db.Column(db.DateTime, nullable=True)
timezone = db.Column(db.String(30), nullable=True)
@@ -189,6 +189,7 @@ class Interaction(db.Model):
question_at = db.Column(db.DateTime, nullable=False)
detailed_question_at = db.Column(db.DateTime, nullable=True)
answer_at = db.Column(db.DateTime, nullable=True)
processing_error = db.Column(db.String(255), nullable=True)
# Relations
embeddings = db.relationship('InteractionEmbedding', backref='interaction', lazy=True)

View File

@@ -0,0 +1,30 @@
import uuid
from typing import Dict, Any, Tuple
from common.utils.celery_utils import current_celery
class SpecialistServices:
@staticmethod
def start_session() -> str:
return f"CHAT_SESSION_{uuid.uuid4()}"
@staticmethod
def execute_specialist(tenant_id, specialist_id, specialist_arguments, session_id, user_timezone) -> Dict[str, Any]:
task = current_celery.send_task(
'execute_specialist',
args=[tenant_id,
specialist_id,
specialist_arguments,
session_id,
user_timezone,
],
queue='llm_interactions'
)
return {
'task_id': task.id,
'status': 'queued',
}

View File

@@ -135,7 +135,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]):
Checks both global and partner-specific directories
"""
# First check the global path
global_path = Path(self._config_dir) / "global" / type_name
global_path = Path(self._config_dir) / "globals" / type_name
# If global path doesn't exist, check if the type exists directly in the root
# (for backward compatibility)
@@ -145,7 +145,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]):
if not global_path.exists():
# Check if it exists in any partner subdirectories
partner_dirs = [d for d in Path(self._config_dir).iterdir()
if d.is_dir() and d.name != "global"]
if d.is_dir() and d.name != "globals"]
for partner_dir in partner_dirs:
partner_type_path = partner_dir / type_name
@@ -178,7 +178,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]):
metadata = yaml_data.get('metadata', {})
# Add partner information if available
partner = None
if "global" not in str(file_path):
if "globals" not in str(file_path):
# Extract partner name from path
# Path format: config_dir/partner_name/type_name/version.yaml
partner = file_path.parent.parent.name

View File

@@ -5,6 +5,7 @@ import markdown
from markupsafe import Markup
from datetime import datetime
from common.utils.nginx_utils import prefixed_url_for as puf
from flask import current_app, url_for
def to_local_time(utc_dt, timezone_str):
@@ -83,8 +84,27 @@ def clean_markdown(text):
return text
def prefixed_url_for(endpoint):
return puf(endpoint)
def prefixed_url_for(endpoint, **kwargs):
return puf(endpoint, **kwargs)
def get_pagination_html(pagination, endpoint, **kwargs):
"""
Generates HTML for pagination with the ability to include additional parameters
"""
html = ['<nav aria-label="Page navigation"><ul class="pagination justify-content-center">']
for page in pagination.iter_pages():
if page:
is_active = 'active' if page == pagination.page else ''
url = url_for(endpoint, page=page, **kwargs)
current_app.logger.debug(f"URL for page {page}: {url}")
html.append(f'<li class="page-item {is_active}"><a class="page-link" href="{url}">{page}</a></li>')
else:
html.append('<li class="page-item disabled"><span class="page-link">...</span></li>')
html.append('</ul></nav>')
return Markup(''.join(html))
def register_filters(app):
@@ -99,3 +119,5 @@ def register_filters(app):
app.jinja_env.filters['clean_markdown'] = clean_markdown
app.jinja_env.globals['prefixed_url_for'] = prefixed_url_for
app.jinja_env.globals['get_pagination_html'] = get_pagination_html

View File

@@ -0,0 +1,29 @@
version: "1.0.0"
name: "Traicie HR BP "
role: >
You are an HR BP (Human Resources Business Partner)
goal: >
As an HR Business Partner, your primary goal is to align people strategies with business objectives. You aim to
ensure that the organisation has the right talent, capabilities, and culture in place to drive performance,
manage change effectively, and support sustainable growth. This involves acting as a trusted advisor to leadership
while advocating for employees and fostering a healthy, high-performing workplace.
{custom_goal}
backstory: >
You didn't start your career as a strategist. You began in traditional HR roles — perhaps as an HR officer or
generalist — mastering recruitment, employee relations, and policy implementation. Over time, you developed a deeper
understanding of how people decisions impact business outcomes.
Through experience, exposure to leadership, and a strong interest in organisational dynamics, you transitioned into a
role that bridges the gap between HR and the business. Youve earned a seat at the table not just by knowing HR
processes, but by understanding the business inside-out, speaking the language of executives, and backing their advice
with data and insight.
You often working side-by-side with senior managers to tackle challenges like workforce planning, leadership
development, organisational change, and employee engagement. Your credibility comes not just from HR knowledge,
but from your ability to co-create solutions that solve real business problems.
{custom_backstory}
full_model_name: "mistral.mistral-medium-latest"
temperature: 0.3
metadata:
author: "Josako"
date_added: "2025-05-21"
description: "HR BP Agent."
changes: "Initial version"

View File

@@ -0,0 +1,19 @@
version: "1.0.0"
name: "Specialist Configuration"
configuration:
specialist_type:
name: "Specialist Type"
type: "str"
description: "The Specialist Type this configuration is made for"
required: True
specialist_version:
name: "Specialist Version"
type: "str"
description: "The Specialist Type version this configuration is made for"
required: True
metadata:
author: "Josako"
date_added: "2025-05-21"
description: "Asset that defines a template in markdown a specialist can process"
changes: "Initial version"

View File

@@ -47,6 +47,7 @@ class TuningLogRecord(logging.LogRecord):
self._tuning_specialist_id = None
self._tuning_retriever_id = None
self._tuning_processor_id = None
self._session_id = None
self.component = os.environ.get('COMPONENT_NAME', 'eveai_app')
def getMessage(self):
@@ -87,16 +88,18 @@ class TuningLogRecord(logging.LogRecord):
'tuning_specialist_id': self._tuning_specialist_id,
'tuning_retriever_id': self._tuning_retriever_id,
'tuning_processor_id': self._tuning_processor_id,
'session_id': self._session_id,
}
def set_tuning_data(self, tenant_id=None, catalog_id=None, specialist_id=None,
retriever_id=None, processor_id=None):
retriever_id=None, processor_id=None, session_id=None,):
"""Set tuning-specific data"""
object.__setattr__(self, '_tuning_tenant_id', tenant_id)
object.__setattr__(self, '_tuning_catalog_id', catalog_id)
object.__setattr__(self, '_tuning_specialist_id', specialist_id)
object.__setattr__(self, '_tuning_retriever_id', retriever_id)
object.__setattr__(self, '_tuning_processor_id', processor_id)
object.__setattr__(self, '_session_id', session_id)
class TuningFormatter(logging.Formatter):
@@ -120,6 +123,12 @@ class TuningFormatter(logging.Formatter):
identifiers.append(f"Catalog: {record.catalog_id}")
if hasattr(record, 'processor_id') and record.processor_id:
identifiers.append(f"Processor: {record.processor_id}")
if hasattr(record, 'specialist_id') and record.specialist_id:
identifiers.append(f"Specialist: {record.specialist_id}")
if hasattr(record, 'retriever_id') and record.retriever_id:
identifiers.append(f"Retriever: {record.retriever_id}")
if hasattr(record, 'session_id') and record.session_id:
identifiers.append(f"Session: {record.session_id}")
formatted_msg = (
f"{formatted_msg}\n"
@@ -149,22 +158,93 @@ class GraylogFormatter(logging.Formatter):
'specialist_id': record.specialist_id,
'retriever_id': record.retriever_id,
'processor_id': record.processor_id,
'session_id': record.session_id,
}
return super().format(record)
class TuningLogger:
"""Helper class to manage tuning logs with consistent structure"""
def __init__(self, logger_name, tenant_id=None, catalog_id=None, specialist_id=None, retriever_id=None, processor_id=None):
def __init__(self, logger_name, tenant_id=None, catalog_id=None, specialist_id=None, retriever_id=None,
processor_id=None, session_id=None, log_file=None):
"""
Initialize a tuning logger
Args:
logger_name: Base name for the logger
tenant_id: Optional tenant ID for context
catalog_id: Optional catalog ID for context
specialist_id: Optional specialist ID for context
retriever_id: Optional retriever ID for context
processor_id: Optional processor ID for context
session_id: Optional session ID for context and log file naming
log_file: Optional custom log file name to use
"""
self.logger = logging.getLogger(logger_name)
self.tenant_id = tenant_id
self.catalog_id = catalog_id
self.specialist_id = specialist_id
self.retriever_id = retriever_id
self.processor_id = processor_id
self.session_id = session_id
self.log_file = log_file
# Determine whether to use a session-specific logger
if session_id:
# Create a unique logger name for this session
session_logger_name = f"{logger_name}_{session_id}"
self.logger = logging.getLogger(session_logger_name)
def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DEBUG):
# If this logger doesn't have handlers yet, configure it
if not self.logger.handlers:
# Determine log file path
if not log_file and session_id:
log_file = f"logs/tuning_{session_id}.log"
elif not log_file:
log_file = "logs/tuning.log"
# Configure the logger
self._configure_session_logger(log_file)
else:
# Use the standard tuning logger
self.logger = logging.getLogger(logger_name)
def _configure_session_logger(self, log_file):
"""Configure a new session-specific logger with appropriate handlers"""
# Create and configure a file handler
file_handler = logging.handlers.RotatingFileHandler(
filename=log_file,
maxBytes=1024 * 1024 * 3, # 3MB
backupCount=3
)
file_handler.setFormatter(TuningFormatter())
file_handler.setLevel(logging.DEBUG)
# Add the file handler to the logger
self.logger.addHandler(file_handler)
# Add Graylog handler in production
env = os.environ.get('FLASK_ENV', 'development')
if env == 'production':
try:
graylog_handler = GELFUDPHandler(
host=GRAYLOG_HOST,
port=GRAYLOG_PORT,
debugging_fields=True
)
graylog_handler.setFormatter(GraylogFormatter())
self.logger.addHandler(graylog_handler)
except Exception as e:
# Fall back to just file logging if Graylog setup fails
fallback_logger = logging.getLogger('eveai_app')
fallback_logger.warning(f"Failed to set up Graylog handler: {str(e)}")
# Set logger level and disable propagation
self.logger.setLevel(logging.DEBUG)
self.logger.propagate = False
def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DEBUG):
"""Log a tuning event with structured data"""
try:
# Create a standard LogRecord for tuning
@@ -186,6 +266,7 @@ class TuningLogger:
record.specialist_id = self.specialist_id
record.retriever_id = self.retriever_id
record.processor_id = self.processor_id
record.session_id = self.session_id
if data:
record.tuning_data = data

View File

@@ -1,6 +1,11 @@
version: "1.0.0"
name: "Management Service"
configuration: {}
configuration:
specialist_denominator:
name: "Specialist Denominator"
type: "string"
description: "Name defining the denominator for the specialist. Needs to be unique."
required: False
permissions: {}
metadata:
author: "Josako"

View File

@@ -1,163 +0,0 @@
version: "1.0.0"
name: "Traicie Vacature Specialist"
framework: "crewai"
configuration:
ko_criteria:
name: "Knock-out criteria"
type: "text"
description: "The knock-out criteria (1 per line)"
required: true
hard_skills:
name: "Hard Skills"
type: "text"
description: "The hard skills to be checked with the applicant (1 per line)"
required: false
soft_skills:
name: "Soft Skills"
type: "text"
description: "The soft skills required for the job (1 per line)"
required: false
tone_of_voice:
name: "Tone of Voice"
type: "enum"
description: "Tone of voice to be used in communicating with the applicant"
required: false
default: "formal"
allowed_values: [ "formal", "informal", "dynamic" ]
vacancy_text:
name: "Vacancy Text"
type: "text"
description: "The vacancy for this specialist"
arguments:
language:
name: "Language"
type: "str"
description: "Language code to be used for receiving questions and giving answers"
required: true
results:
rag_output:
answer:
name: "answer"
type: "str"
description: "Answer to the query"
required: true
citations:
name: "citations"
type: "List[str]"
description: "List of citations"
required: false
insufficient_info:
name: "insufficient_info"
type: "bool"
description: "Whether or not the query is insufficient info"
required: true
spin:
situation:
name: "situation"
type: "str"
description: "A description of the customer's current situation / context"
required: false
problem:
name: "problem"
type: "str"
description: "The current problems the customer is facing, for which he/she seeks a solution"
required: false
implication:
name: "implication"
type: "str"
description: "A list of implications"
required: false
needs:
name: "needs"
type: "str"
description: "A list of needs"
required: false
additional_info:
name: "additional_info"
type: "str"
description: "Additional information that may be commercially interesting"
required: false
lead_info:
lead_personal_info:
name:
name: "name"
type: "str"
description: "name of the lead"
required: "true"
job_title:
name: "job_title"
type: "str"
description: "job title"
required: false
email:
name: "email"
type: "str"
description: "lead email"
required: "false"
phone:
name: "phone"
type: "str"
description: "lead phone"
required: false
additional_info:
name: "additional_info"
type: "str"
description: "additional info on the lead"
required: false
lead_company_info:
company_name:
name: "company_name"
type: "str"
description: "Name of the lead company"
required: false
industry:
name: "industry"
type: "str"
description: "The industry of the company"
required: false
company_size:
name: "company_size"
type: "int"
description: "The size of the company"
required: false
company_website:
name: "company_website"
type: "str"
description: "The main website for the company"
required: false
additional_info:
name: "additional_info"
type: "str"
description: "Additional information that may be commercially interesting"
required: false
agents:
- type: "RAG_AGENT"
version: "1.0"
- type: "RAG_COMMUNICATION_AGENT"
version: "1.0"
- type: "SPIN_DETECTION_AGENT"
version: "1.0"
- type: "SPIN_SALES_SPECIALIST_AGENT"
version: "1.0"
- type: "IDENTIFICATION_AGENT"
version: "1.0"
- type: "RAG_COMMUNICATION_AGENT"
version: "1.0"
tasks:
- type: "RAG_TASK"
version: "1.0"
- type: "SPIN_DETECT_TASK"
version: "1.0"
- type: "SPIN_QUESTIONS_TASK"
version: "1.0"
- type: "IDENTIFICATION_DETECTION_TASK"
version: "1.0"
- type: "IDENTIFICATION_QUESTIONS_TASK"
version: "1.0"
- type: "RAG_CONSOLIDATION_TASK"
version: "1.0"
metadata:
author: "Josako"
date_added: "2025-01-08"
changes: "Initial version"
description: "A Specialist that performs both Q&A as SPIN (Sales Process) activities"

View File

@@ -1,6 +1,7 @@
version: "1.0.0"
name: "RAG Specialist"
framework: "crewai"
chat: true
configuration:
name:
name: "name"

View File

@@ -1,6 +1,7 @@
version: "1.0.0"
name: "Spin Sales Specialist"
framework: "crewai"
chat: true
configuration:
name:
name: "name"

View File

Before

Width:  |  Height:  |  Size: 387 KiB

After

Width:  |  Height:  |  Size: 387 KiB

View File

@@ -1,6 +1,7 @@
version: 1.0.0
name: "Standard RAG Specialist"
framework: "langchain"
chat: true
configuration:
specialist_context:
name: "Specialist Context"

View File

@@ -0,0 +1,36 @@
version: "1.0.0"
name: "Traicie Vacancy Definition Specialist"
framework: "crewai"
partner: "traicie"
chat: false
configuration: {}
arguments:
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
criteria:
name: "criteria"
type: "List[str, str]"
description: "List of vacancy knock out criteria and their descriptions"
required: false
agents:
- type: "TRAICIE_HR_BP_AGENT"
version: "1.0"
tasks:
- type: "TRAICIE_GET_COMPETENCIES_TASK"
version: "1.0"
- type: "TRAICIE_GET_KO_CRITERIA_TASK"
version: "1.0"
metadata:
author: "Josako"
date_added: "2025-05-21"
changes: "Initial version"
description: "Assistant to create a new Vacancy based on Vacancy Text"

View File

@@ -0,0 +1,28 @@
version: "1.0.0"
name: "Get Competencies"
task_description: >
You are provided with a vacancy text, in beween triple backquotes.
Identify and list all explicitly stated competencies, skills, knowledge, qualifications, and requirements mentioned in
the vacancy text. This includes:
• Technical skills
• Education or training
• Work experience
• Language proficiency
• Certifications or driving licences
• Personal characteristics
Restrict yourself strictly to what is literally stated or clearly described in the job posting.
Respect the language of the vacancy text, and return answers / output in the same language.
{custom_description}
Vacancy Text:
```{vacancy_text}```
expected_output: >
A list of title and description of the competencies for the given vacancy text.
{custom_expected_output}
metadata:
author: "Josako"
date_added: "2025-01-25"
description: "A Task to collect all behavioural competencies from a vacancy text"
changes: "Initial version"

View File

@@ -0,0 +1,37 @@
version: "1.0.0"
name: "Get KO Criteria"
task_description: >
You are provided with a vacancy text, in beween triple backquotes.
Use logical reasoning based on the realities of the job, taking into account:
• The job title
• The content of the job description
• Typical characteristics of similar roles
Identify the minimum requirements that are absolutely essential to perform the job properly even if they are not
explicitly stated in the text.
Assess the job within its specific context and ask yourself questions such as:
• Does the job require physical stamina?
• Is weekend or shift work involved?
• Is contact with certain materials (e.g. meat, chemicals) unavoidable?
• Is independent working essential?
• Is knowledge of a specific language or system critical for customer interaction or safety?
• Are there any specific characteristics, contexts, or requirements so obvious that they are often left unstated, yet essential to perform the job?
Create a prioritised list of the 5 most critical knock-out criteria, ranked by importance.
Treat this as a logical and professional reasoning exercise.
Respect the language of the vacancy text, and return answers / output in the same language.
{custom_description}
Vacancy Text:
```{vacancy_text}```
expected_output: >
A list of title and description of the (knock-out) criteria for the given vacancy text.
{custom_expected_output}
metadata:
author: "Josako"
date_added: "2025-01-25"
description: "A Task to collect all KO criteria from a vacancy text"
changes: "Initial version"

View File

@@ -28,4 +28,9 @@ AGENT_TYPES = {
"name": "SPIN Sales Specialist",
"description": "An Agent that asks for Follow-up questions for SPIN-process",
},
"TRAICIE_HR_BP_AGENT": {
"name": "Traicie HR BP Agent",
"description": "An HR Business Partner Agent",
"partner": "traicie"
}
}

View File

@@ -4,4 +4,8 @@ AGENT_TYPES = {
"name": "Document Template",
"description": "Asset that defines a template in markdown a specialist can process",
},
"SPECIALIST_CONFIGURATION": {
"name": "Specialist Configuration",
"description": "Asset that defines a specialist configuration",
},
}

View File

@@ -12,9 +12,9 @@ SPECIALIST_TYPES = {
"name": "Spin Sales Specialist",
"description": "A specialist that allows to answer user queries, try to get SPIN-information and Identification",
},
"TRAICIE_VACATURE_SPECIALIST": {
"name": "Traicie Vacature Specialist",
"description": "Specialist configureerbaar voor een specifieke vacature",
"partner": "Traicie"
"TRAICIE_VACANCY_DEFINITION_SPECIALIST": {
"name": "Traicie Vacancy Definition Specialist",
"description": "Assistant to create a new Vacancy based on Vacancy Text",
"partner": "traicie"
}
}

View File

@@ -31,5 +31,15 @@ TASK_TYPES = {
"RAG_CONSOLIDATION_TASK": {
"name": "RAG Consolidation",
"description": "A Task to consolidate questions and answers",
},
"TRAICIE_GET_COMPETENCIES_TASK": {
"name": "Traicie Get Competencies",
"description": "A Task to get Competencies from a Vacancy Text",
"partner": "traicie"
},
"TRAICIE_GET_KO_CRITERIA_TASK": {
"name": "Traicie Get KO Criteria",
"description": "A Task to get KO Criteria from a Vacancy Text",
"partner": "traicie"
}
}

View File

@@ -10,6 +10,7 @@ from common.utils.celery_utils import current_celery
from common.utils.execution_progress import ExecutionProgressTracker
from eveai_api.api.auth import requires_service
from common.models.interaction import Specialist
from common.services.interaction.specialist_services import SpecialistServices
specialist_execution_ns = Namespace('specialist-execution', description='Specialist execution operations')
@@ -24,7 +25,7 @@ class StartSession(Resource):
@requires_service("SPECIALIST_API")
@specialist_execution_ns.response(201, 'New Session ID created Successfully', specialist_start_session_response)
def get(self):
new_session_id = f"{uuid.uuid4()}"
new_session_id = SpecialistServices.start_session()
return {
'session_id': new_session_id,
}, 201
@@ -56,22 +57,16 @@ class StartExecution(Resource):
data = specialist_execution_ns.payload
# Send task to queue
task = current_celery.send_task(
'execute_specialist',
args=[tenant_id,
data['specialist_id'],
data['arguments'],
data['session_id'],
data['user_timezone'],
],
queue='llm_interactions'
)
result = SpecialistServices.execute_specialist(
tenant_id=tenant_id,
specialist_id=data['specialist_id'],
specialist_arguments=data['arguments'],
session_id=data['session_id'],
user_timezone=data['user_timezone'])
return {
'task_id': task.id,
'status': 'queued',
'stream_url': f'/api/v1/specialist-execution/{task.id}/stream'
}, 201
result['stream_url'] = f"/api/v1/specialist-execution/{result['task_id']}/stream"
return result, 201
@specialist_execution_ns.route('/<string:task_id>/stream')

View File

@@ -3,6 +3,8 @@ import traceback
import jinja2
from flask import render_template, request, jsonify, redirect, current_app, flash
from flask_login import current_user
from common.utils.eveai_exceptions import EveAINoSessionTenant
from common.utils.nginx_utils import prefixed_url_for
@@ -67,6 +69,24 @@ def attribute_error_handler(error):
error_details=error_msg), 500
def no_tenant_selected_error(error):
"""Handle errors when no tenant is selected in the current session.
This typically happens when a session expires or becomes invalid after
a long period of inactivity. The user will be redirected to the login page.
"""
current_app.logger.error(f"No Session Tenant Error: {error}")
flash('Your session expired. You will have to re-enter your credentials', 'warning')
# Perform logout if user is authenticated
if current_user.is_authenticated:
from flask_security.utils import logout_user
logout_user()
# Redirect to login page
return redirect(prefixed_url_for('security.login'))
def general_exception(e):
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
flash('An application error occurred. The technical team has been notified.', 'error')
@@ -80,6 +100,7 @@ def register_error_handlers(app):
app.register_error_handler(500, internal_server_error)
app.register_error_handler(401, not_authorised_error)
app.register_error_handler(403, not_authorised_error)
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
app.register_error_handler(KeyError, key_error_handler)
app.register_error_handler(AttributeError, attribute_error_handler)
app.register_error_handler(Exception, general_exception)

View File

@@ -14,6 +14,7 @@
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="view_chat_session" class="btn btn-primary" onclick="return validateTableSelection('chatSessionsForm')">View Chat Session</button>
<button type="submit" name="action" value="chat_session_interactions" class="btn btn-primary" onclick="return validateTableSelection('chatSessionsForm')">View Chat Session interactions</button>
</div>
</div>
</form>

View File

@@ -84,7 +84,7 @@
<div class="card">
<div class="card-body">
{{ render_selectable_table(
headers=["Agent ID", "Name", "Type", "Status"],
headers=["Agent ID", "Name", "Type", "Type Version"],
rows=agent_rows if agent_rows else [],
selectable=True,
id="agentsTable",
@@ -105,7 +105,7 @@
<div class="card">
<div class="card-body">
{{ render_selectable_table(
headers=["Task ID", "Name", "Type", "Status"],
headers=["Task ID", "Name", "Type", "Type Version"],
rows=task_rows if task_rows else [],
selectable=True,
id="tasksTable",
@@ -126,7 +126,7 @@
<div class="card">
<div class="card-body">
{{ render_selectable_table(
headers=["Tool ID", "Name", "Type", "Status"],
headers=["Tool ID", "Name", "Type", "Type Version"],
rows=tool_rows if tool_rows else [],
selectable=True,
id="toolsTable",

View File

@@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field %}
{% block title %}Execute Specialist{% endblock %}
{% block content_title %}Execute Specialist{% endblock %}
{% block content_description %}Execute a Specialist{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = [] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<button type="submit" class="btn btn-primary">Execute Specialist</button>
</form>
{% endblock %}
{% block content_footer %}
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Chat Sessions Interactions{% endblock %}
{% block content_title %}Chat Sessions{% endblock %}
{% block content_description %}View Chat Sessions for Tenant{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('interaction_bp.handle_chat_session_selection') }}" id="chatSessionsForm">
{{ render_selectable_table(headers=["ID", "Question At", "Detailed Question At", "Answer At", "Processing Error"], rows=rows, selectable=False, id="interactionsTable") }}
{# <div class="form-group mt-3 d-flex justify-content-between">#}
{# <div>#}
{# <button type="submit" name="action" value="view_chat_session" class="btn btn-primary" onclick="return validateTableSelection('chatSessionsForm')">View Chat Session</button>#}
{# </div>#}
{# </div>#}
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ get_pagination_html(pagination, 'interaction_bp.session_interactions_by_session_id', session_id=chat_session.session_id) }}
{% endblock %}

View File

@@ -0,0 +1,192 @@
{% extends "base.html" %}
{% block title %}Specialist Execution Status{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4>Specialist Execution Status</h4>
{% if specialist %}
<span class="badge bg-primary">{{ specialist.name }}</span>
{% endif %}
</div>
<div class="card-body">
<div class="progress mb-4">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
<div class="mb-4">
<h5>Current Status:</h5>
<div id="current-status" class="alert alert-info">Execution starting...</div>
</div>
<div class="mb-4">
<h5>Execution Log:</h5>
<div id="execution-log" class="border p-3 bg-light" style="max-height: 300px; overflow-y: auto;">
<div class="text-muted">Waiting for updates...</div>
</div>
</div>
<div id="completion-actions" class="d-none">
<hr>
<h5>Execution Completed</h5>
<p>The specialist has completed processing.</p>
<a id="view-results-btn" href="{{ prefixed_url_for('interaction_bp.view_chat_session_by_session_id', session_id=chat_session_id) }}" class="btn btn-primary">View Results</a>
<a href="{{ prefixed_url_for('interaction_bp.specialists') }}" class="btn btn-secondary">Back to Specialists</a>
</div>
<div id="error-actions" class="d-none">
<hr>
<h5>Error Occurred</h5>
<p>An error occurred during specialist execution.</p>
<div id="error-details" class="alert alert-danger"></div>
<a href="{{ prefixed_url_for('interaction_bp.specialists') }}" class="btn btn-secondary">Back to Specialists</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const taskId = "{{ task_id }}";
const executionLog = document.getElementById('execution-log');
const currentStatus = document.getElementById('current-status');
const progressBar = document.getElementById('progress-bar');
const completionActions = document.getElementById('completion-actions');
const errorActions = document.getElementById('error-actions');
const errorDetails = document.getElementById('error-details');
let logEntries = [];
let isComplete = false;
// Connect to the SSE stream
const eventSource = new EventSource("{{ prefixed_url_for('interaction_bp.specialist_execution_updates', task_id=task_id) }}");
// General data handler
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
addLogEntry(data);
updateStatus(data);
};
// Specific event handlers
eventSource.addEventListener('Task Started', function(event) {
const data = JSON.parse(event.data);
progressBar.style.width = '10%';
currentStatus.textContent = 'Task started: ' + data.data.message;
currentStatus.className = 'alert alert-info';
});
eventSource.addEventListener('Task Progress', function(event) {
const data = JSON.parse(event.data);
// Update progress percentage if available
if (data.data.percentage) {
progressBar.style.width = data.data.percentage + '%';
} else {
// Incremental progress if no percentage available
const currentWidth = parseInt(progressBar.style.width) || 0;
progressBar.style.width = Math.min(currentWidth + 5, 90) + '%';
}
});
eventSource.addEventListener('Task Complete', function(event) {
const data = JSON.parse(event.data);
progressBar.style.width = '100%';
progressBar.className = 'progress-bar bg-success';
currentStatus.textContent = 'Processing completed: ' + data.data.message;
currentStatus.className = 'alert alert-success';
completionActions.classList.remove('d-none');
isComplete = true;
eventSource.close();
});
eventSource.addEventListener('Task Error', function(event) {
const data = JSON.parse(event.data);
progressBar.className = 'progress-bar bg-danger';
currentStatus.textContent = 'Error occurred';
currentStatus.className = 'alert alert-danger';
errorDetails.textContent = data.data.error;
errorActions.classList.remove('d-none');
eventSource.close();
});
eventSource.addEventListener('EveAI Specialist Complete', function(event) {
const data = JSON.parse(event.data);
progressBar.style.width = '100%';
progressBar.className = 'progress-bar bg-success';
currentStatus.textContent = 'Specialist processing completed';
currentStatus.className = 'alert alert-success';
completionActions.classList.remove('d-none');
isComplete = true;
eventSource.close();
});
eventSource.onerror = function(event) {
// Only report an error if the task is not already completed
if (!isComplete) {
currentStatus.textContent = 'Connection to server lost';
currentStatus.className = 'alert alert-warning';
errorDetails.textContent = 'The connection to the server was lost. The task may still be running.';
errorActions.classList.remove('d-none');
}
};
function addLogEntry(data) {
const timestamp = new Date(data.timestamp).toLocaleTimeString();
const message = data.data.message || JSON.stringify(data.data);
const type = data.processing_type;
// Add the log entry to the array
logEntries.push({timestamp, message, type});
// Limit to last 100 entries
if (logEntries.length > 100) {
logEntries.shift();
}
// Refresh the log element
renderLog();
}
function renderLog() {
executionLog.innerHTML = '';
logEntries.forEach(entry => {
const entryElement = document.createElement('div');
entryElement.className = 'log-entry mb-1';
// Determine the appropriate badge color based on type
let badgeClass = 'bg-secondary';
if (entry.type === 'Task Started') badgeClass = 'bg-primary';
if (entry.type === 'Task Progress') badgeClass = 'bg-info';
if (entry.type === 'Task Complete' || entry.type === 'EveAI Specialist Complete') badgeClass = 'bg-success';
if (entry.type === 'Task Error') badgeClass = 'bg-danger';
entryElement.innerHTML = `
<span class="badge ${badgeClass}">${entry.timestamp}</span>
<span class="ms-2">${entry.message}</span>
`;
executionLog.appendChild(entryElement);
});
// Scroll to bottom
executionLog.scrollTop = executionLog.scrollHeight;
}
function updateStatus(data) {
// Update the current status with the latest information
if (data.data.message) {
currentStatus.textContent = data.data.message;
}
}
});
</script>
{% endblock %}

View File

@@ -14,6 +14,7 @@
<div class="form-group mt-3 d-flex justify-content-between">
<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="execute_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Execute Specialist</button>
</div>
<button type="submit" name="action" value="create_specialist" class="btn btn-success">Register Specialist</button>
</div>

View File

@@ -50,19 +50,18 @@
{% if specialist_arguments %}
<div class="mb-4">
<h6 class="mb-3">Specialist Arguments:</h6>
<div class="code-wrapper">
<pre><code class="language-json" style="width: 100%;">{{ specialist_arguments | tojson(indent=2) }}</code></pre>
</div>
<div id="args-viewer-{{ loop.index }}" class="json-viewer" style="height: 300px; width: 100%;"></div>
<div id="args-viewer-{{ loop.index }}-data" class="d-none">{{ specialist_arguments | tojson(indent=2) }}</div>
</div>
{% endif %}
<!-- Results Section -->
{% if specialist_results %}
<div class="mb-4">
<h6 class="mb-3">Specialist Results:</h6>
<div class="code-wrapper">
<pre><code class="language-json" style="width: 100%;">{{ specialist_results | tojson(indent=2) }}</code></pre>
</div>
<div id="results-viewer-{{ loop.index }}" class="json-viewer" style="height: 300px; width: 100%;"></div>
<div id="results-viewer-{{ loop.index }}-data" class="d-none">{{ specialist_results | tojson(indent=2) }}</div>
</div>
{% endif %}
@@ -94,167 +93,42 @@
</div>
</div>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.interaction-header {
font-size: 0.9rem;
display: flex;
flex-direction: column;
width: 100%;
padding: 0.5rem 0;
}
.interaction-metadata {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
}
.interaction-time {
font-size: 0.9rem;
}
.specialist-info {
display: flex;
gap: 0.5rem;
align-items: center;
}
.interaction-question {
font-size: 0.9rem;
font-weight: bold;
line-height: 1.4;
}
.badge {
font-size: 0.9rem;
padding: 0.35em 0.65em;
white-space: nowrap;
}
.accordion-button {
padding: 0.5rem 1rem;
}
.accordion-button::after {
margin-left: 1rem;
}
.json-display {
background-color: #f8f9fa;
border-radius: 4px;
padding: 15px;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: monospace;
font-size: 0.85rem;
line-height: 1.5;
max-width: 100%;
overflow-x: auto;
}
.list-group-item {
font-size: 0.9rem;
}
.material-icons {
font-size: 1.1rem;
}
pre {
margin: 0;
padding: 0;
white-space: pre-wrap !important; /* Force wrapping */
word-wrap: break-word !important; /* Break long words if necessary */
max-width: 100%; /* Ensure container doesn't overflow */
}
pre, code {
margin: 0;
padding: 0;
white-space: pre-wrap !important; /* Force wrapping */
word-wrap: break-word !important; /* Break long words if necessary */
max-width: 100%; /* Ensure container doesn't overflow */
}
pre code {
padding: 1rem !important;
border-radius: 4px;
font-size: 0.75rem;
line-height: 1.5;
white-space: pre-wrap !important; /* Force wrapping in code block */
}
.code-wrapper {
position: relative;
width: 100%;
}
/* Override all possible highlight.js white-space settings */
.code-wrapper pre,
.code-wrapper pre code,
.code-wrapper pre code.hljs,
.code-wrapper .hljs {
white-space: pre-wrap !important;
overflow-wrap: break-word !important;
word-wrap: break-word !important;
word-break: break-word !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
.code-wrapper pre {
margin: 0;
background: #f8f9fa;
border-radius: 4px;
}
.code-wrapper pre code {
padding: 1rem !important;
font-family: monospace;
font-size: 0.9rem;
line-height: 1.5;
display: block;
}
/* Override highlight.js default nowrap behavior */
.hljs {
background: #f8f9fa !important;
white-space: pre-wrap !important;
word-wrap: break-word !important;
}
/* Color theme */
.hljs-string {
color: #0a3069 !important;
}
.hljs-attr {
color: #953800 !important;
}
.hljs-number {
color: #116329 !important;
}
.hljs-boolean {
color: #0550ae !important;
}
</style>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize syntax highlighting
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
// JSONEditor initialiseren wanneer een accordion item wordt geopend
const accordionButtons = document.querySelectorAll('.accordion-button');
accordionButtons.forEach(function(button, index) {
button.addEventListener('click', function() {
// Voeg een kleine vertraging toe om te zorgen dat de accordion volledig is geopend
setTimeout(function() {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
// Als de json-viewer class niet wordt gebruikt, handmatige initialisatie
const loopIndex = index + 1;
// Controleer of er elementen zijn die niet automatisch zijn geïnitialiseerd
// Dit is een backup voor het geval de automatische initialisatie niet werkt
const containers = document.querySelectorAll(`#collapse${loopIndex} .json-editor-container`);
containers.forEach(function(container) {
if (!container.classList.contains('jsoneditor-initialized')) {
const dataElement = document.getElementById(`${container.id}-data`);
if (dataElement) {
try {
const jsonData = JSON.parse(dataElement.value || dataElement.textContent);
window.EveAI.JsonEditors.initializeReadOnly(container.id, jsonData);
} catch (e) {
console.error(`Error parsing JSON for ${container.id}:`, e);
}
}
}
});
}
}, 300);
});
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block title %}Waiting for Chat Session{% endblock %}
{% block content_title %}Chat Session Being Created{% endblock %}
{% block content_description %}Please wait...{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block extra_head %}
<meta http-equiv="refresh" content="2;url={{ refresh_url }}">
<style>
.spinner {
width: 40px;
height: 40px;
margin: 30px auto;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #3498db;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block content %}
<div class="container text-center">
<div class="card">
<div class="card-body">
<h4 class="card-title">Chat Session is being created</h4>
<div class="spinner"></div>
<p class="mt-3">The specialist is being executed and the chat session is being created.</p>
<p>Session ID: <code>{{ session_id }}</code></p>
<p>This page will automatically refresh every 2 seconds...</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -357,42 +357,47 @@
</div>
{% endmacro %}
{% macro render_pagination(pagination, endpoint) %}
<nav aria-label="Page navigation">
<ul class="pagination">
<!-- Previous Button -->
<li class="page-item {{ 'disabled' if not pagination.has_prev }}">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.prev_num) if pagination.has_prev else 'javascript:;' }}" tabindex="-1">
<span class="material-symbols-outlined">keyboard_double_arrow_left</span>
{# <span class="sr-only">Previous</span>#}
</a>
</li>
<!-- Page Number Buttons -->
{% for page in pagination.iter_pages(left_edge=1, left_current=2, right_current=3, right_edge=1) %}
<li class="page-item {{ 'active' if page == pagination.page }}">
<a class="page-link" href="{{ url_for(endpoint, page=page) }}">
{% if page == pagination.page %}
<span class="material-symbols-outlined">target</span>
{# <span class="sr-only">(current)</span>#}
{% else %}
{{ page }}
{% endif %}
</a>
</li>
{% endfor %}
<!-- Next Button -->
<li class="page-item {{ 'disabled' if not pagination.has_next }}">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.next_num) if pagination.has_next else 'javascript:;' }}">
<span class="material-symbols-outlined">keyboard_double_arrow_right</span>
{#{% macro render_pagination(pagination, endpoint) %}#}
{#<nav aria-label="Page navigation">#}
{# <ul class="pagination">#}
{# <!-- Previous Button -->#}
{# <li class="page-item {{ 'disabled' if not pagination.has_prev }}">#}
{# <a class="page-link" href="{{ url_for(endpoint, page=pagination.prev_num) if pagination.has_prev else 'javascript:;' }}" tabindex="-1">#}
{# <span class="material-symbols-outlined">keyboard_double_arrow_left</span>#}
{# </a>#}
{# </li>#}
{##}
{# <!-- Page Number Buttons -->#}
{# {% for page in pagination.iter_pages(left_edge=1, left_current=2, right_current=3, right_edge=1) %}#}
{# <li class="page-item {{ 'active' if page == pagination.page }}">#}
{# <a class="page-link" href="{{ url_for(endpoint, page=page) }}">#}
{# {% if page == pagination.page %}#}
{# <span class="material-symbols-outlined">target</span>#}
{# {% else %}#}
{# {{ page }}#}
{# {% endif %}#}
{# </a>#}
{# </li>#}
{# {% endfor %}#}
{##}
{# <!-- Next Button -->#}
{# <li class="page-item {{ 'disabled' if not pagination.has_next }}">#}
{# <a class="page-link" href="{{ url_for(endpoint, page=pagination.next_num) if pagination.has_next else 'javascript:;' }}">#}
{# <span class="material-symbols-outlined">keyboard_double_arrow_right</span>#}
{# <span class="sr-only">Next</span>#}
</a>
</li>
</ul>
</nav>
{# </a>#}
{# </li>#}
{# </ul>#}
{#</nav>#}
{#{% endmacro %}#}
{% macro render_pagination(pagination, endpoint) %}
{# Deze macro is een wrapper rond de globale get_pagination_html functie #}
{# Ondersteunt nu expliciet een session_id parameter #}
{{ get_pagination_html(pagination, endpoint) }}
{% endmacro %}
{% macro render_filter_field(field_name, label, options, current_value) %}
<div class="form-group">
<label for="{{ field_name }}">{{ label }}</label>

View File

@@ -17,6 +17,188 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.1.0/jsoneditor.min.css" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.1.0/jsoneditor.min.js"></script>
<script>
window.EveAI = window.EveAI || {};
// Centraal beheer van JSONEditor instanties
window.EveAI.JsonEditors = {
instances: {},
// Initialiseer een nieuwe JSONEditor
initialize: function(containerId, data, options = {}) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`Container with ID ${containerId} not found`);
return null;
}
if (this.instances[containerId]) {
console.log(`JSONEditor for ${containerId} already initialized`);
return this.instances[containerId];
}
// Bepaal of de editor in read-only modus moet worden weergegeven
const isReadOnly = options.readOnly === true;
// Standaard opties
const defaultOptions = {
mode: isReadOnly ? 'tree' : 'tree', // Gebruik 'tree' voor read-only om expand/collapse te behouden
modes: isReadOnly ? ['tree', 'view'] : ['tree', 'code', 'view'], // Voeg 'tree' toe aan read-only modes
search: true,
navigationBar: false,
mainMenuBar: true, // Behoud menubar voor expand/collapse knoppen
statusBar: !isReadOnly // Verberg alleen statusbar in read-only modus
};
// Als expliciet onEditable=false is ingesteld, zorg ervoor dat de editor read-only is
if (options.onEditable === false || options.onEditable && typeof options.onEditable === 'function') {
defaultOptions.onEditable = function() { return false; };
}
// Combineer standaard opties met aangepaste opties
const editorOptions = {...defaultOptions, ...options};
try {
const editor = new JSONEditor(container, editorOptions, data);
container.classList.add('jsoneditor-initialized');
// Voeg read-only klasse toe indien nodig
if (isReadOnly) {
container.classList.add('jsoneditor-readonly-mode');
} else {
container.classList.add('jsoneditor-edit-mode');
}
this.instances[containerId] = editor;
return editor;
} catch (e) {
console.error(`Error initializing JSONEditor for ${containerId}:`, e);
container.innerHTML = `<div class="alert alert-danger p-3">
<strong>Error loading JSON data:</strong><br>${e.message}
</div>`;
return null;
}
},
// Initialiseer een read-only JSONEditor (handige functie)
initializeReadOnly: function(containerId, data, additionalOptions = {}) {
const readOnlyOptions = {
readOnly: true,
mode: 'tree', // Gebruik tree mode voor navigatie
modes: ['tree', 'view'], // Beperk tot tree en view
expandAll: true, // Alles openklappen bij initialisatie
onEditable: function() { return false; }
};
// Combineer read-only opties met eventuele aanvullende opties
const options = {...readOnlyOptions, ...additionalOptions};
return this.initialize(containerId, data, options);
},
// Haal een bestaande instantie op of maak een nieuwe aan
get: function(containerId, data, options = {}) {
if (this.instances[containerId]) {
return this.instances[containerId];
}
return this.initialize(containerId, data, options);
},
// Verwijder een instantie
destroy: function(containerId) {
if (this.instances[containerId]) {
this.instances[containerId].destroy();
delete this.instances[containerId];
const container = document.getElementById(containerId);
if (container) {
container.classList.remove('jsoneditor-initialized');
container.classList.remove('jsoneditor-readonly-mode');
container.classList.remove('jsoneditor-edit-mode');
}
}
}
};
// Zoek en initialiseer standaard JSON editors bij DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Initialiseer JSON editors voor tekstgebieden met de klasse 'json-editor'
document.querySelectorAll('.json-editor').forEach(function(textarea) {
// Controleer of er een bijbehorende container is
const containerId = textarea.id + '-editor';
const container = document.getElementById(containerId);
if (container) {
try {
// Parse de JSON-data uit het tekstgebied
const data = textarea.value ? JSON.parse(textarea.value) : {};
// Controleer of de editor in read-only modus moet worden getoond
const isReadOnly = textarea.readOnly || textarea.hasAttribute('readonly') ||
textarea.classList.contains('readonly');
// Bepaal de juiste editor-opties op basis van de read-only status
const editorOptions = {
mode: isReadOnly ? 'tree' : 'code', // Gebruik tree voor read-only
modes: isReadOnly ? ['tree', 'view'] : ['code', 'tree'],
readOnly: isReadOnly,
onEditable: function() { return !isReadOnly; },
onChangeText: isReadOnly ? undefined : function(jsonString) {
textarea.value = jsonString;
}
};
// Initialiseer de editor
const editor = window.EveAI.JsonEditors.initialize(containerId, data, editorOptions);
// Voeg validatie toe als het een bewerkbare editor is
if (!isReadOnly && editor) {
editor.validate().then(function(errors) {
if (errors.length) {
container.style.border = '2px solid red';
} else {
container.style.border = '1px solid #ccc';
}
});
}
} catch (e) {
console.error('Error parsing initial JSON:', e);
container.innerHTML = `<div class="alert alert-danger p-3">
<strong>Error loading JSON data:</strong><br>${e.message}
</div>`;
}
}
});
// Initialiseer JSON editors voor containers met de klasse 'json-viewer' (alleen-lezen)
document.querySelectorAll('.json-viewer').forEach(function(container) {
const dataElement = document.getElementById(container.id + '-data');
if (dataElement) {
try {
// Parse de JSON-data
const data = dataElement.textContent ? JSON.parse(dataElement.textContent) : {};
// Initialiseer een read-only editor met tree-mode voor navigatie
window.EveAI.JsonEditors.initializeReadOnly(container.id, data);
} catch (e) {
console.error('Error parsing JSON for viewer:', e);
container.innerHTML = `<div class="alert alert-danger p-3">
<strong>Error loading JSON data:</strong><br>${e.message}
</div>`;
}
}
});
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltips
@@ -156,6 +338,24 @@ function validateTableSelection(formId) {
return true;
}
</script>
<script>
$(document).ready(function() {
// Maak tabelrijen klikbaar (voor tabellen met radio-buttons)
$(document).on('click', 'table tbody tr', function(e) {
// Voorkom dat dit gedrag optreedt als er direct op de radio-button of een link wordt geklikt
if (!$(e.target).is('input[type="radio"], a')) {
// Vind de radio-button in deze rij
const radio = $(this).find('input[type="radio"]');
// Selecteer de radio-button
radio.prop('checked', true);
// Voeg visuele feedback toe voor de gebruiker
$('table tbody tr').removeClass('table-active');
$(this).addClass('table-active');
}
});
});
</script>
<style>
.json-editor-container {
height: 400px;

View File

@@ -81,6 +81,8 @@ class DynamicFormBase(FlaskForm):
message=f"Value must be between {min_value or '-∞'} and {max_value or ''}"
)
)
elif field_type in ['string', 'str']:
validators_list.append(self._validate_string_not_empty)
elif field_type == 'tagging_fields':
validators_list.append(self._validate_tagging_fields)
elif field_type == 'tagging_fields_filter':
@@ -130,6 +132,11 @@ class DynamicFormBase(FlaskForm):
except Exception as e:
raise ValidationError(f"Invalid filter definition: {str(e)}")
def _validate_string_not_empty(self, form, field):
"""Validator om te controleren of een StringField niet leeg is"""
if not field.data or field.data.strip() == "":
raise ValidationError("This field cannot be empty")
def _validate_filter_condition(self, condition):
"""Recursively validate a filter condition structure"""
# Check if this is a logical condition (AND/OR/NOT)
@@ -264,6 +271,7 @@ class DynamicFormBase(FlaskForm):
'float': FloatField,
'boolean': BooleanField,
'string': StringField,
'str': StringField,
'text': TextAreaField,
'date': DateField,
'file': FileField,
@@ -368,6 +376,28 @@ class DynamicFormBase(FlaskForm):
data[original_field_name] = field.data
return data
# def validate_on_submit(self):
# """Aangepaste validate_on_submit die dynamische velden correct verwerkt"""
# if request.method == 'POST':
# # Update formdata met de huidige request data
# self.formdata = request.form
# self.raw_formdata = request.form.to_dict()
#
# # Verwerk alle dynamische velden opnieuw met de actuele formuliergegevens
# for collection_name, field_names in self.dynamic_fields.items():
# for full_field_name in field_names:
# field = self._fields.get(full_field_name)
# if field:
# # Log voor debug
# current_app.logger.debug(
# f"Re-processing dynamic field {full_field_name} with current form data")
# # Verwerk het veld opnieuw met de huidige request data
# field.process(self.formdata)
#
# # Nu voeren we de standaard validatie uit
# return super().validate()
# return False
def validate_tagging_fields(form, field):
"""Validate the tagging fields structure"""

View File

@@ -1,6 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
from wtforms.fields.datetime import DateField
from wtforms.fields.numeric import IntegerField
from wtforms.validators import DataRequired, Length, Optional
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
@@ -124,3 +125,11 @@ class EditEveAIAssetVersionForm(DynamicFormBase):
asset_type_version = StringField('Asset Type Version', validators=[DataRequired()], render_kw={'readonly': True})
bucket_name = StringField('Bucket Name', validators=[DataRequired()], render_kw={'readonly': True})
class ExecuteSpecialistForm(DynamicFormBase):
id = IntegerField('Specialist ID', validators=[DataRequired()], render_kw={'readonly': True})
name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})

View File

@@ -1,5 +1,7 @@
import ast
import json
from datetime import datetime as dt, timezone as tz
import time
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for
from flask_security import roles_accepted
@@ -14,7 +16,9 @@ from common.models.interaction import (ChatSession, Interaction, InteractionEmbe
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
from common.extensions import db, cache_manager
from common.services.interaction.specialist_services import SpecialistServices
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
from common.utils.execution_progress import ExecutionProgressTracker
from common.utils.model_logging_utils import set_logging_information, update_logging_information
from common.utils.middleware import mw_before_request
@@ -25,7 +29,7 @@ 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)
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm)
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
@@ -72,10 +76,13 @@ def handle_chat_session_selection():
cs_id = ast.literal_eval(chat_session_identification).get('value')
action = request.form['action']
current_app.logger.debug(f'Handle Chat Session Selection Action: {action}')
match action:
case 'view_chat_session':
return redirect(prefixed_url_for('interaction_bp.view_chat_session', chat_session_id=cs_id))
case 'chat_session_interactions':
return redirect(prefixed_url_for('interaction_bp.session_interactions', chat_session_id=cs_id))
# Add more conditions for other actions
return redirect(prefixed_url_for('interaction_bp.chat_sessions'))
@@ -124,8 +131,14 @@ def view_chat_session(chat_session_id):
@interaction_bp.route('/view_chat_session_by_session_id/<session_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_chat_session_by_session_id(session_id):
"""
Deze route accepteert een session_id (string) en stuurt door naar view_chat_session met het juiste chat_session_id (integer)
"""
# Vind de chat session op basis van session_id
chat_session = ChatSession.query.filter_by(session_id=session_id).first_or_404()
show_chat_session(chat_session)
# Nu we het chat_session object hebben, kunnen we de bestaande functie hergebruiken
return view_chat_session(chat_session.id)
def show_chat_session(chat_session):
@@ -303,6 +316,8 @@ def handle_specialist_selection():
if action == "edit_specialist":
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=specialist_id))
elif action == "execute_specialist":
return redirect(prefixed_url_for('interaction_bp.execute_specialist', specialist_id=specialist_id))
return redirect(prefixed_url_for('interaction_bp.specialists'))
@@ -391,9 +406,8 @@ def edit_tool(tool_id):
form = EditEveAIToolForm(obj=tool)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('interaction/components/edit_tool.html',
form=form,
tool=tool)
return render_template('interaction/components/edit_tool.html', form=form, tool=tool)
return None
@interaction_bp.route('/tool/<int:tool_id>/save', methods=['POST'])
@@ -546,3 +560,106 @@ def edit_asset_version(asset_version_id):
return render_template('interaction/edit_asset_version.html', form=form)
@interaction_bp.route('/execute_specialist/<int:specialist_id>', methods=['GET', 'POST'])
def execute_specialist(specialist_id):
specialist = Specialist.query.get_or_404(specialist_id)
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
if specialist_config.get('chat', True):
flash("Only specialists that don't require interactions can be executed this way!", 'error')
return redirect(prefixed_url_for('interaction_bp.specialists'))
form = ExecuteSpecialistForm(request.form, obj=specialist)
arguments_config = specialist_config.get('arguments', None)
if arguments_config:
form.add_dynamic_fields('arguments', arguments_config)
if form.validate_on_submit():
# We're only interested in gathering the dynamic arguments
arguments = form.get_dynamic_data("arguments")
current_app.logger.debug(f"Executing specialist {specialist.id} with arguments: {arguments}")
session_id = SpecialistServices.start_session()
execution_task = SpecialistServices.execute_specialist(
tenant_id=session.get('tenant').get('id'),
specialist_id=specialist_id,
specialist_arguments=arguments,
session_id=session_id,
user_timezone=session.get('tenant').get('timezone')
)
current_app.logger.debug(f"Execution task for specialist {specialist.id} created: {execution_task}")
return redirect(prefixed_url_for('interaction_bp.session_interactions_by_session_id', session_id=session_id))
return render_template('interaction/execute_specialist.html', form=form)
@interaction_bp.route('/session_interactions_by_session_id/<session_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def session_interactions_by_session_id(session_id):
"""
This route shows all interactions for a given session_id (string).
If the chat_session doesn't exist yet, it will wait for up to 10 seconds
(with 1 second intervals) until it becomes available.
"""
waiting_message = request.args.get('waiting', 'false') == 'true'
# Try up to 10 times with 1 second pause
max_tries = 10
current_try = 1
while current_try <= max_tries:
chat_session = ChatSession.query.filter_by(session_id=session_id).first()
if chat_session:
# Session found, display the interactions
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = Interaction.query.filter_by(chat_session_id=chat_session.id).order_by(Interaction.question_at)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
interactions = pagination.items
rows = prepare_table_for_macro(interactions, [('id', ''), ('question_at', ''), ('detailed_question_at', ''),
('answer_at', ''), ('processing_error', '')])
# Define a callback to make a URL for a given page and the same session_id
def make_page_url(page_num):
return prefixed_url_for('interaction_bp.session_interactions_by_session_id', session_id=session_id,
page=page_num)
return render_template('interaction/session_interactions.html',
chat_session=chat_session, rows=rows, pagination=pagination,
make_page_url=make_page_url)
# Session not found, wait and try again
if current_try < max_tries:
current_try += 1
time.sleep(1)
else:
# Maximum number of attempts reached
break
# If we're here, the session wasn't found after the maximum number of attempts
flash(f'The chat session with ID {session_id} could not be found after {max_tries} attempts. '
f'The session may still be in the process of being created or the ID might be incorrect.', 'warning')
# Show a waiting page with auto-refresh if we haven't shown a waiting message yet
if not waiting_message:
return render_template('interaction/waiting_for_session.html',
session_id=session_id,
refresh_url=prefixed_url_for('interaction_bp.session_interactions_by_session_id',
session_id=session_id,
waiting='true'))
# If we've already shown a waiting message and still don't have a session, go back to the specialists page
return redirect(prefixed_url_for('interaction_bp.specialists'))
@interaction_bp.route('/session_interactions/<chat_session_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def session_interactions(chat_session_id):
"""
This route shows all interactions for a given chat_session_id (int).
"""
chat_session = ChatSession.query.get_or_404(chat_session_id)
return session_interactions_by_session_id(chat_session.session_id)

View File

@@ -163,22 +163,24 @@ def edit_partner_service(partner_service_id):
partner_id = session['partner']['id']
form = EditPartnerServiceForm(obj=partner_service)
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
partner_service.type_version)
configuration_config = partner_service_config.get('configuration')
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
f"{configuration_config}")
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
permissions_config = partner_service_config.get('permissions')
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
f"{permissions_config}")
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
if request.method == 'GET':
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
partner_service.type_version)
configuration_config = partner_service_config.get('configuration')
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
f"{configuration_config}")
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
permissions_config = partner_service_config.get('permissions')
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
f"{permissions_config}")
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
if form.validate_on_submit():
if request.method == 'POST':
current_app.logger.debug(f"Form returned: {form.data}")
raw_form_data = request.form.to_dict()
current_app.logger.debug(f"Raw form data: {raw_form_data}")
if form.validate_on_submit():
form.populate_obj(partner_service)
partner_service.configuration = form.get_dynamic_data('configuration')
partner_service.permissions = form.get_dynamic_data('permissions')

View File

@@ -37,8 +37,6 @@ class TenantForm(FlaskForm):
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
# initialise LLM fields
self.llm_model.choices = [(model, model) for model in current_app.config['SUPPORTED_LLMS']]
# Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
# Show field only for Super Users with partner in session

View File

@@ -302,7 +302,6 @@ def handle_tenant_selection():
# set tenant information in the session
session['tenant'] = the_tenant.to_dict()
session['default_language'] = the_tenant.default_language
session['llm_model'] = the_tenant.llm_model
# remove catalog-related items from the session
session.pop('catalog_id', None)
session.pop('catalog_name', None)

View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel, Field
class ListItem(BaseModel):
title: str = Field(..., description="The title or name of the item")
description: str = Field(..., description="A descriptive explanation of the item")

View File

@@ -0,0 +1,13 @@
from typing import List, Optional
from pydantic import BaseModel, Field
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
# class BehaviouralCompetence(BaseModel):
# title: str = Field(..., description="The title of the behavioural competence.")
# description: Optional[str] = Field(None, description="The description of the behavioural competence.")
class BehaviouralCompetencies(BaseModel):
competencies: List[ListItem] = Field(
default_factory=list,
description="A list of behavioural competencies."
)

View File

@@ -0,0 +1,13 @@
from typing import List, Optional
from pydantic import BaseModel, Field
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
# class KnockoutCriterion(BaseModel):
# title: str = Field(..., description="The title of the knockout criterion.")
# description: Optional[str] = Field(None, description="Further explanation of the knockout criterion.")
class KnockoutCriteria(BaseModel):
criteria: List[ListItem] = Field(
default_factory=list,
description="A prioritized list of the 5 most critical knockout criteria, ranked by importance."
)

View File

@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
from typing import Dict, Any, List
from flask import current_app
from common.extensions import cache_manager
from common.models.interaction import SpecialistRetriever
from common.utils.execution_progress import ExecutionProgressTracker
from config.logging_config import TuningLogger
@@ -75,6 +76,8 @@ class BaseSpecialistExecutor(ABC):
'tuning',
tenant_id=self.tenant_id,
specialist_id=self.specialist_id,
session_id=self.session_id,
log_file=f"logs/tuning_{self.session_id}.log"
)
# Verify logger is working with a test message
if self.tuning:
@@ -101,6 +104,13 @@ class BaseSpecialistExecutor(ABC):
def get_specialist_class(specialist_type: str, type_version: str):
major_minor = '_'.join(type_version.split('.')[:2])
module_path = f"eveai_chat_workers.specialists.{specialist_type}.{major_minor}"
specialist_config = cache_manager.specialists_config_cache.get_config(specialist_type, type_version)
partner = specialist_config.get("partner", None)
current_app.logger.debug(f"Specialist partner for {specialist_type} {type_version} is {partner}")
if partner:
module_path = f"eveai_chat_workers.specialists.{partner}.{specialist_type}.{major_minor}"
else:
module_path = f"eveai_chat_workers.specialists.{specialist_type}.{major_minor}"
current_app.logger.debug(f"Importing specialist class from {module_path}")
module = importlib.import_module(module_path)
return module.SpecialistExecutor

View File

@@ -77,6 +77,9 @@ class EveAICrewAICrew(Crew):
model_config = ConfigDict(arbitrary_types_allowed=True)
def __init__(self, specialist, name: str, **kwargs):
if specialist.tuning:
log_file = f"logs/crewai/{specialist.session_id}_{specialist.task_id}.txt"
super().__init__(**kwargs)
self.specialist = specialist
self.name = name

View File

@@ -244,7 +244,7 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
current_app.logger.error(f"Error detailing question: {e}")
return question # Fallback to original question
def _retrieve_context(self, arguments: SpecialistArguments) -> Tuple[str, List[int]]:
def _retrieve_context(self, arguments: SpecialistArguments) -> tuple[str, list[dict[str, Any]]]:
with current_event.create_span("Specialist Retrieval"):
self.log_tuning("Starting context retrieval", {
"num_retrievers": len(self.retrievers),
@@ -326,26 +326,29 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
raise NotImplementedError
def execute_specialist(self, arguments: SpecialistArguments) -> SpecialistResult:
# Detail the incoming query
if self._cached_session.interactions:
query = arguments.query
language = arguments.language
detailed_query = self._detail_question(language, query)
current_app.logger.debug(f"Retrievers for this specialist: {self.retrievers}")
if self.retrievers:
# Detail the incoming query
if self._cached_session.interactions:
query = arguments.query
language = arguments.language
detailed_query = self._detail_question(language, query)
else:
detailed_query = arguments.query
modified_arguments = {
"query": detailed_query,
"original_query": arguments.query
}
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,
"citations": citations,
}
final_result = result.model_copy(update=modified_result)
else:
detailed_query = arguments.query
modified_arguments = {
"query": detailed_query,
"original_query": arguments.query
}
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,
"citations": citations,
}
final_result = result.model_copy(update=modified_result)
final_result = self.execute(arguments, "", [])
return final_result

View File

@@ -10,7 +10,7 @@ from common.utils.business_event_context import current_event
from eveai_chat_workers.retrievers.retriever_typing import RetrieverArguments
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.rag.rag_v1_0 import RAGOutput
from eveai_chat_workers.outputs.globals.rag.rag_v1_0 import RAGOutput
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState

View File

@@ -14,9 +14,9 @@ from common.utils.business_event_context import current_event
from eveai_chat_workers.retrievers.retriever_typing import RetrieverArguments
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.identification.identification_v1_0 import LeadInfoOutput
from eveai_chat_workers.outputs.spin.spin_v1_0 import SPINOutput
from eveai_chat_workers.outputs.rag.rag_v1_0 import RAGOutput
from eveai_chat_workers.outputs.globals.identification.identification_v1_0 import LeadInfoOutput
from eveai_chat_workers.outputs.globals.spin.spin_v1_0 import SPINOutput
from eveai_chat_workers.outputs.globals.rag.rag_v1_0 import RAGOutput
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
from common.utils.pydantic_utils import flatten_pydantic_model

View File

@@ -0,0 +1,158 @@
import asyncio
import json
from os import wait
from typing import Optional, List
from crewai.flow.flow import start, listen, and_
from crewai import Process
from flask import current_app
from gevent import sleep
from pydantic import BaseModel, Field
from common.extensions import cache_manager
from common.models.user import Tenant
from common.utils.business_event_context import current_event
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
from eveai_chat_workers.retrievers.retriever_typing import RetrieverArguments
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_0 import BehaviouralCompetencies
from eveai_chat_workers.outputs.traicie.knockout_criteria.knockout_criteria_v1_0 import KnockoutCriteria
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
from common.utils.pydantic_utils import flatten_pydantic_model
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"""
type: TRAICIE_VACANCY_DEFINITION_SPECIALIST
type_version: 1.0
Traicie Vacancy Definition Specialist Executor class
"""
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
self.vac_def_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_VACANCY_DEFINITION_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")
self._add_task_agent("traicie_get_ko_criteria_task", "traicie_hr_bp_agent")
def _config_pydantic_outputs(self):
self._add_pydantic_output("traicie_get_competencies_task", BehaviouralCompetencies, "competencies")
self._add_pydantic_output("traicie_get_ko_criteria_task", KnockoutCriteria, "criteria")
def _instantiate_specialist(self):
verbose = self.tuning
vac_def_agents = [self.traicie_hr_bp_agent]
vac_def_tasks = [self.traicie_get_competencies_task, self.traicie_get_ko_criteria_task]
self.vac_def_crew = EveAICrewAICrew(
self,
"Vacancy Definition Crew",
agents=vac_def_agents,
tasks=vac_def_tasks,
verbose=verbose,
)
self.flow = VacancyDefinitionFlow(
self,
self.vac_def_crew
)
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
self.log_tuning("Traicie Vacancy Definition Specialist execution started", {})
flow_inputs = {
"vacancy_text": arguments.vacancy_text,
}
flow_results = self.flow.kickoff(inputs=flow_inputs)
flow_state = self.flow.state
results = VacancyDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
update_data = {}
if flow_state.competencies:
results.competencies = flow_state.competencies
if flow_state.criteria:
results.criteria = flow_state.criteria
self.log_tuning(f"Traicie Vacancy Definition Specialist execution ended", {"Results": results.model_dump()})
return results
class VacancyDefinitionSpecialistInput(BaseModel):
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
class VacancyDefinitionSpecialistResult(SpecialistResult):
competencies: Optional[List[ListItem]] = None
criteria: Optional[List[ListItem]] = None
class VacancyDefFlowState(EveAIFlowState):
"""Flow state for Traicie Vacancy Definition specialist that automatically updates from task outputs"""
input: Optional[VacancyDefinitionSpecialistInput] = None
competencies: Optional[List[ListItem]] = None
criteria: Optional[List[ListItem]] = None
class VacancyDefinitionFlow(EveAICrewAIFlow[VacancyDefFlowState]):
def __init__(self,
specialist_executor: CrewAIBaseSpecialistExecutor,
vac_def_crew: EveAICrewAICrew,
**kwargs):
super().__init__(specialist_executor, "Traicie Vacancy Definition Specialist Flow", **kwargs)
self.specialist_executor = specialist_executor
self.vac_def_crew = vac_def_crew
self.exception_raised = False
@start()
def process_inputs(self):
return ""
@listen(process_inputs)
async def execute_vac_def(self):
inputs = self.state.input.model_dump()
try:
current_app.logger.debug("In execute_vac_def")
crew_output = await self.vac_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.vac_def_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
elif task.name == "traicie_get_ko_criteria_task":
# update["criteria"] = task.output.pydantic.criteria
self.state.criteria = task.output.pydantic.criteria
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
current_app.logger.debug(f"State after execute_vac_def: {self.state}")
current_app.logger.debug(f"State dump after execute_vac_def: {self.state.model_dump()}")
return crew_output
except Exception as e:
current_app.logger.error(f"CREW execute_vac_def 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}")
self.state.input = VacancyDefinitionSpecialistInput.model_validate(inputs)
result = await super().kickoff_async(inputs)
return self.state

View File

@@ -253,6 +253,7 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
current_app.logger.info(
f'execute_specialist: Processing request for tenant {tenant_id} using specialist {specialist_id}')
new_interaction = None
try:
# Ensure we have a session
cached_session = cache_manager.chat_session_cache.get_cached_session(
@@ -331,6 +332,13 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
except Exception as e:
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
current_app.logger.error(f'execute_specialist: Error executing specialist: {e}')
new_interaction.processing_error = str(e)[:255]
try:
db.session.add(new_interaction)
db.session.commit()
except SQLAlchemyError as e:
current_app.logger.error(f'execute_specialist: Error updating interaction: {e}')
raise

View File

@@ -0,0 +1,29 @@
"""Add processing_error field to Interaction
Revision ID: 55c696c4a687
Revises: 6ae8e7ca1d42
Create Date: 2025-05-26 06:50:34.942006
"""
from alembic import op
import sqlalchemy as sa
import pgvector
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '55c696c4a687'
down_revision = '6ae8e7ca1d42'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('interaction', sa.Column('processing_error', sa.String(length=255), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('interaction', 'processing_error')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""lenthen ChatSession ID
Revision ID: 6ae8e7ca1d42
Revises: 4a9f7a6285cc
Create Date: 2025-05-22 13:25:55.807958
"""
from alembic import op
import sqlalchemy as sa
import pgvector
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '6ae8e7ca1d42'
down_revision = '4a9f7a6285cc'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('chat_session', 'session_id',
existing_type=sa.VARCHAR(length=36),
type_=sa.String(length=49),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@@ -651,4 +651,263 @@ select.select2[multiple] {
opacity: 1;
}
.interaction-header {
font-size: 0.9rem;
display: flex;
flex-direction: column;
width: 100%;
padding: 0.5rem 0;
}
.interaction-metadata {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
}
.interaction-time {
font-size: 0.9rem;
}
.specialist-info {
display: flex;
gap: 0.5rem;
align-items: center;
}
.interaction-question {
font-size: 0.9rem;
font-weight: bold;
line-height: 1.4;
}
.badge {
font-size: 0.9rem;
padding: 0.35em 0.65em;
white-space: nowrap;
}
.accordion-button {
padding: 0.5rem 1rem;
}
.accordion-button::after {
margin-left: 1rem;
}
.list-group-item {
font-size: 0.9rem;
}
.material-icons {
font-size: 1.1rem;
}
/* Aanpassingen voor JSONEditor */
.json-editor-container {
height: 400px;
margin-bottom: 1rem;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e9ecef;
}
/* Hoofdmenu styling */
.jsoneditor-menu {
background-color: #ee912e !important;
border-bottom: 1px solid #ee912e !important;
color: #495057 !important;
}
/* Speciaal voor read-only modus */
.jsoneditor-readonly-mode .jsoneditor-menu {
background-color: #ee912e !important;
border-bottom: 1px solid #ee912e !important;
}
/* Verberg bewerkingsknoppen in readonly mode, maar behoud expand/collapse knoppen */
.jsoneditor-readonly-mode .jsoneditor-modes,
.jsoneditor-readonly-mode button.jsoneditor-separator,
.jsoneditor-readonly-mode button.jsoneditor-repair,
.jsoneditor-readonly-mode button.jsoneditor-undo,
.jsoneditor-readonly-mode button.jsoneditor-redo,
.jsoneditor-readonly-mode button.jsoneditor-compact,
.jsoneditor-readonly-mode button.jsoneditor-sort,
.jsoneditor-readonly-mode button.jsoneditor-transform {
display: none !important;
}
/* Toon belangrijke navigatieknoppen in read-only modus */
.jsoneditor-readonly-mode .jsoneditor-expand-all,
.jsoneditor-readonly-mode .jsoneditor-collapse-all,
.jsoneditor-readonly-mode .jsoneditor-search {
display: inline-block !important;
}
/* Verberg alle bewerkingselementen in de tree-view */
.jsoneditor-readonly-mode td.jsoneditor-tree button.jsoneditor-button.jsoneditor-contextmenu-button {
visibility: hidden !important;
}
/* Behoud wel de expand/collapse knoppen in de tree */
.jsoneditor-readonly-mode td.jsoneditor-tree button.jsoneditor-button.jsoneditor-expanded,
.jsoneditor-readonly-mode td.jsoneditor-tree button.jsoneditor-button.jsoneditor-collapsed {
visibility: visible !important;
}
/* Knoppen in het menu */
.jsoneditor-menu button {
background-color: transparent !important;
color: #495057 !important;
border: none !important;
}
.jsoneditor-menu button:hover {
background-color: #e8a34d !important;
}
/* Mode selector (Tree ▾) */
.jsoneditor-modes button {
background-color: transparent !important;
color: #495057 !important;
border: none !important;
padding: 4px 10px !important;
}
.jsoneditor-modes button:hover {
background-color: #e8a34d !important;
}
/* Zoekbalk */
.jsoneditor-search {
background-color: transparent !important;
}
.jsoneditor-search input {
border: 1px solid #ced4da !important;
border-radius: 4px !important;
color: #495057 !important;
padding: 4px 8px !important;
}
.jsoneditor-search button {
background-color: transparent !important;
color: #495057 !important;
}
/* Binnengebied */
.jsoneditor-outer {
border: none !important;
background-color: #fff !important;
}
/* Tree view */
.jsoneditor-tree {
background-color: #fff !important;
color: #212529 !important;
}
/* Read-only tree achtergrond */
.jsoneditor-readonly-mode .jsoneditor-tree {
background-color: #f8f9fa !important;
}
/* Value & field styling */
div.jsoneditor-field, div.jsoneditor-value {
color: #212529 !important;
}
div.jsoneditor-value.jsoneditor-string {
color: #0d6efd !important; /* Bootstrap primary color voor strings */
}
div.jsoneditor-value.jsoneditor-number {
color: #198754 !important; /* Bootstrap success color voor getallen */
}
div.jsoneditor-value.jsoneditor-boolean {
color: #dc3545 !important; /* Bootstrap danger color voor booleans */
}
div.jsoneditor-value.jsoneditor-null {
color: #6c757d !important; /* Bootstrap secondary color voor null */
}
/* Expand/collapse knoppen */
.jsoneditor-button.jsoneditor-expanded,
.jsoneditor-button.jsoneditor-collapsed {
filter: brightness(0.8) !important;
}
/* Context menu buttons (drie puntjes) */
.jsoneditor-button.jsoneditor-contextmenu-button {
filter: brightness(0.8) !important;
}
/* Verberg context menu knoppen in read-only modus */
.jsoneditor-readonly-mode .jsoneditor-contextmenu-button {
display: none !important;
}
/* Hover effect op rijen */
.jsoneditor-tree tr:hover {
background-color: rgba(0, 123, 255, 0.05) !important;
}
/* Geselecteerde rij */
.jsoneditor-tree tr.jsoneditor-selected {
background-color: rgba(0, 123, 255, 0.1) !important;
}
/* Consistent font */
.jsoneditor, .jsoneditor-tree, div.jsoneditor-field, div.jsoneditor-value {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
font-size: 0.875rem !important;
}
/* Consistente separators */
td.jsoneditor-separator {
color: #6c757d !important;
}
/* Contextmenu indien nodig */
div.jsoneditor-contextmenu {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
border-radius: 4px !important;
border: 1px solid rgba(0, 0, 0, 0.15) !important;
}
div.jsoneditor-contextmenu ul li button {
color: #212529 !important;
}
div.jsoneditor-contextmenu ul li button:hover {
background-color: #f8f9fa !important;
}
/* Onzichtbare dragarea buttons verbergen voor een schoner uiterlijk */
.jsoneditor-button.jsoneditor-dragarea {
visibility: hidden !important;
}
/* Alleen tonen bij hover */
tr:hover .jsoneditor-button.jsoneditor-dragarea {
visibility: visible !important;
opacity: 0.6 !important;
}
/* Verberg de textarea met JSON-data */
.d-none {
display: none !important;
}
/* Zorg ervoor dat de JSONEditor goed zichtbaar is */
.jsoneditor {
width: 100% !important;
height: 100% !important;
border: none !important;
}

View File

@@ -35,7 +35,7 @@ langchain-text-splitters~=0.3.8
langcodes~=3.4.0
langdetect~=1.0.9
langsmith~=0.1.81
openai~=1.77.0
openai~=1.75.0
pg8000~=1.31.2
pgvector~=0.2.5
pycryptodome~=3.20.0
@@ -82,11 +82,11 @@ typing_extensions~=4.12.2
babel~=2.16.0
dogpile.cache~=1.3.3
python-docx~=1.1.2
crewai~=0.118.0
crewai~=0.121.0
sseclient~=0.0.27
termcolor~=2.5.0
mistral-common~=1.5.3
mistralai~=1.6.0
mistral-common~=1.5.5
mistralai~=1.7.1
contextvars~=2.4
pandas~=2.2.3
prometheus_client~=0.21.1