- 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:
@@ -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)
|
||||
|
||||
30
common/services/interaction/specialist_services.py
Normal file
30
common/services/interaction/specialist_services.py
Normal 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',
|
||||
}
|
||||
|
||||
|
||||
6
common/utils/cache/config_cache.py
vendored
6
common/utils/cache/config_cache.py
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
29
config/agents/traicie/TRAICIE_HR_BP_AGENT/1.0.0.yaml
Normal file
29
config/agents/traicie/TRAICIE_HR_BP_AGENT/1.0.0.yaml
Normal 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. You’ve 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"
|
||||
19
config/assets/globals/SPECIALIST_CONFIGURATION/1.0.0.yaml
Normal file
19
config/assets/globals/SPECIALIST_CONFIGURATION/1.0.0.yaml
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -1,6 +1,7 @@
|
||||
version: "1.0.0"
|
||||
name: "RAG Specialist"
|
||||
framework: "crewai"
|
||||
chat: true
|
||||
configuration:
|
||||
name:
|
||||
name: "name"
|
||||
@@ -1,6 +1,7 @@
|
||||
version: "1.0.0"
|
||||
name: "Spin Sales Specialist"
|
||||
framework: "crewai"
|
||||
chat: true
|
||||
configuration:
|
||||
name:
|
||||
name: "name"
|
||||
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 387 KiB |
@@ -1,6 +1,7 @@
|
||||
version: 1.0.0
|
||||
name: "Standard RAG Specialist"
|
||||
framework: "langchain"
|
||||
chat: true
|
||||
configuration:
|
||||
specialist_context:
|
||||
name: "Specialist Context"
|
||||
@@ -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"
|
||||
|
Before Width: | Height: | Size: 812 KiB After Width: | Height: | Size: 812 KiB |
@@ -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"
|
||||
37
config/tasks/traicie/TRAICIE_GET_KO_CRITERIA_TASK/1.0.0.yaml
Normal file
37
config/tasks/traicie/TRAICIE_GET_KO_CRITERIA_TASK/1.0.0.yaml
Normal 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"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
23
eveai_app/templates/interaction/execute_specialist.html
Normal file
23
eveai_app/templates/interaction/execute_specialist.html
Normal 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 %}
|
||||
25
eveai_app/templates/interaction/session_interactions.html
Normal file
25
eveai_app/templates/interaction/session_interactions.html
Normal 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 %}
|
||||
192
eveai_app/templates/interaction/specialist_execution_status.html
Normal file
192
eveai_app/templates/interaction/specialist_execution_status.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,166 +93,41 @@
|
||||
</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>
|
||||
|
||||
42
eveai_app/templates/interaction/waiting_for_session.html
Normal file
42
eveai_app/templates/interaction/waiting_for_session.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -163,6 +163,7 @@ def edit_partner_service(partner_service_id):
|
||||
partner_id = session['partner']['id']
|
||||
|
||||
form = EditPartnerServiceForm(obj=partner_service)
|
||||
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')
|
||||
@@ -174,11 +175,12 @@ def edit_partner_service(partner_service_id):
|
||||
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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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])
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +326,8 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
|
||||
raise NotImplementedError
|
||||
|
||||
def execute_specialist(self, arguments: SpecialistArguments) -> SpecialistResult:
|
||||
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
|
||||
@@ -341,11 +343,12 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
|
||||
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:
|
||||
final_result = self.execute(arguments, "", [])
|
||||
|
||||
return final_result
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user