- 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):
|
class ChatSession(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=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_start = db.Column(db.DateTime, nullable=False)
|
||||||
session_end = db.Column(db.DateTime, nullable=True)
|
session_end = db.Column(db.DateTime, nullable=True)
|
||||||
timezone = db.Column(db.String(30), 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)
|
question_at = db.Column(db.DateTime, nullable=False)
|
||||||
detailed_question_at = db.Column(db.DateTime, nullable=True)
|
detailed_question_at = db.Column(db.DateTime, nullable=True)
|
||||||
answer_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
|
# Relations
|
||||||
embeddings = db.relationship('InteractionEmbedding', backref='interaction', lazy=True)
|
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
|
Checks both global and partner-specific directories
|
||||||
"""
|
"""
|
||||||
# First check the global path
|
# 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
|
# If global path doesn't exist, check if the type exists directly in the root
|
||||||
# (for backward compatibility)
|
# (for backward compatibility)
|
||||||
@@ -145,7 +145,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]):
|
|||||||
if not global_path.exists():
|
if not global_path.exists():
|
||||||
# Check if it exists in any partner subdirectories
|
# Check if it exists in any partner subdirectories
|
||||||
partner_dirs = [d for d in Path(self._config_dir).iterdir()
|
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:
|
for partner_dir in partner_dirs:
|
||||||
partner_type_path = partner_dir / type_name
|
partner_type_path = partner_dir / type_name
|
||||||
@@ -178,7 +178,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]):
|
|||||||
metadata = yaml_data.get('metadata', {})
|
metadata = yaml_data.get('metadata', {})
|
||||||
# Add partner information if available
|
# Add partner information if available
|
||||||
partner = None
|
partner = None
|
||||||
if "global" not in str(file_path):
|
if "globals" not in str(file_path):
|
||||||
# Extract partner name from path
|
# Extract partner name from path
|
||||||
# Path format: config_dir/partner_name/type_name/version.yaml
|
# Path format: config_dir/partner_name/type_name/version.yaml
|
||||||
partner = file_path.parent.parent.name
|
partner = file_path.parent.parent.name
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import markdown
|
|||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from common.utils.nginx_utils import prefixed_url_for as puf
|
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):
|
def to_local_time(utc_dt, timezone_str):
|
||||||
@@ -83,8 +84,27 @@ def clean_markdown(text):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def prefixed_url_for(endpoint):
|
def prefixed_url_for(endpoint, **kwargs):
|
||||||
return puf(endpoint)
|
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):
|
def register_filters(app):
|
||||||
@@ -99,3 +119,5 @@ def register_filters(app):
|
|||||||
app.jinja_env.filters['clean_markdown'] = clean_markdown
|
app.jinja_env.filters['clean_markdown'] = clean_markdown
|
||||||
|
|
||||||
app.jinja_env.globals['prefixed_url_for'] = prefixed_url_for
|
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_specialist_id = None
|
||||||
self._tuning_retriever_id = None
|
self._tuning_retriever_id = None
|
||||||
self._tuning_processor_id = None
|
self._tuning_processor_id = None
|
||||||
|
self._session_id = None
|
||||||
self.component = os.environ.get('COMPONENT_NAME', 'eveai_app')
|
self.component = os.environ.get('COMPONENT_NAME', 'eveai_app')
|
||||||
|
|
||||||
def getMessage(self):
|
def getMessage(self):
|
||||||
@@ -87,16 +88,18 @@ class TuningLogRecord(logging.LogRecord):
|
|||||||
'tuning_specialist_id': self._tuning_specialist_id,
|
'tuning_specialist_id': self._tuning_specialist_id,
|
||||||
'tuning_retriever_id': self._tuning_retriever_id,
|
'tuning_retriever_id': self._tuning_retriever_id,
|
||||||
'tuning_processor_id': self._tuning_processor_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,
|
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"""
|
"""Set tuning-specific data"""
|
||||||
object.__setattr__(self, '_tuning_tenant_id', tenant_id)
|
object.__setattr__(self, '_tuning_tenant_id', tenant_id)
|
||||||
object.__setattr__(self, '_tuning_catalog_id', catalog_id)
|
object.__setattr__(self, '_tuning_catalog_id', catalog_id)
|
||||||
object.__setattr__(self, '_tuning_specialist_id', specialist_id)
|
object.__setattr__(self, '_tuning_specialist_id', specialist_id)
|
||||||
object.__setattr__(self, '_tuning_retriever_id', retriever_id)
|
object.__setattr__(self, '_tuning_retriever_id', retriever_id)
|
||||||
object.__setattr__(self, '_tuning_processor_id', processor_id)
|
object.__setattr__(self, '_tuning_processor_id', processor_id)
|
||||||
|
object.__setattr__(self, '_session_id', session_id)
|
||||||
|
|
||||||
|
|
||||||
class TuningFormatter(logging.Formatter):
|
class TuningFormatter(logging.Formatter):
|
||||||
@@ -120,6 +123,12 @@ class TuningFormatter(logging.Formatter):
|
|||||||
identifiers.append(f"Catalog: {record.catalog_id}")
|
identifiers.append(f"Catalog: {record.catalog_id}")
|
||||||
if hasattr(record, 'processor_id') and record.processor_id:
|
if hasattr(record, 'processor_id') and record.processor_id:
|
||||||
identifiers.append(f"Processor: {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 = (
|
formatted_msg = (
|
||||||
f"{formatted_msg}\n"
|
f"{formatted_msg}\n"
|
||||||
@@ -149,22 +158,93 @@ class GraylogFormatter(logging.Formatter):
|
|||||||
'specialist_id': record.specialist_id,
|
'specialist_id': record.specialist_id,
|
||||||
'retriever_id': record.retriever_id,
|
'retriever_id': record.retriever_id,
|
||||||
'processor_id': record.processor_id,
|
'processor_id': record.processor_id,
|
||||||
|
'session_id': record.session_id,
|
||||||
}
|
}
|
||||||
return super().format(record)
|
return super().format(record)
|
||||||
|
|
||||||
|
|
||||||
class TuningLogger:
|
class TuningLogger:
|
||||||
"""Helper class to manage tuning logs with consistent structure"""
|
"""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.logger = logging.getLogger(logger_name)
|
||||||
self.tenant_id = tenant_id
|
self.tenant_id = tenant_id
|
||||||
self.catalog_id = catalog_id
|
self.catalog_id = catalog_id
|
||||||
self.specialist_id = specialist_id
|
self.specialist_id = specialist_id
|
||||||
self.retriever_id = retriever_id
|
self.retriever_id = retriever_id
|
||||||
self.processor_id = processor_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"""
|
"""Log a tuning event with structured data"""
|
||||||
try:
|
try:
|
||||||
# Create a standard LogRecord for tuning
|
# Create a standard LogRecord for tuning
|
||||||
@@ -186,6 +266,7 @@ class TuningLogger:
|
|||||||
record.specialist_id = self.specialist_id
|
record.specialist_id = self.specialist_id
|
||||||
record.retriever_id = self.retriever_id
|
record.retriever_id = self.retriever_id
|
||||||
record.processor_id = self.processor_id
|
record.processor_id = self.processor_id
|
||||||
|
record.session_id = self.session_id
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
record.tuning_data = data
|
record.tuning_data = data
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
name: "Management Service"
|
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: {}
|
permissions: {}
|
||||||
metadata:
|
metadata:
|
||||||
author: "Josako"
|
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"
|
version: "1.0.0"
|
||||||
name: "RAG Specialist"
|
name: "RAG Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "name"
|
name: "name"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
name: "Spin Sales Specialist"
|
name: "Spin Sales Specialist"
|
||||||
framework: "crewai"
|
framework: "crewai"
|
||||||
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "name"
|
name: "name"
|
||||||
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 387 KiB |
@@ -1,6 +1,7 @@
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
name: "Standard RAG Specialist"
|
name: "Standard RAG Specialist"
|
||||||
framework: "langchain"
|
framework: "langchain"
|
||||||
|
chat: true
|
||||||
configuration:
|
configuration:
|
||||||
specialist_context:
|
specialist_context:
|
||||||
name: "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",
|
"name": "SPIN Sales Specialist",
|
||||||
"description": "An Agent that asks for Follow-up questions for SPIN-process",
|
"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",
|
"name": "Document Template",
|
||||||
"description": "Asset that defines a template in markdown a specialist can process",
|
"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",
|
"name": "Spin Sales Specialist",
|
||||||
"description": "A specialist that allows to answer user queries, try to get SPIN-information and Identification",
|
"description": "A specialist that allows to answer user queries, try to get SPIN-information and Identification",
|
||||||
},
|
},
|
||||||
"TRAICIE_VACATURE_SPECIALIST": {
|
"TRAICIE_VACANCY_DEFINITION_SPECIALIST": {
|
||||||
"name": "Traicie Vacature Specialist",
|
"name": "Traicie Vacancy Definition Specialist",
|
||||||
"description": "Specialist configureerbaar voor een specifieke vacature",
|
"description": "Assistant to create a new Vacancy based on Vacancy Text",
|
||||||
"partner": "Traicie"
|
"partner": "traicie"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,5 +31,15 @@ TASK_TYPES = {
|
|||||||
"RAG_CONSOLIDATION_TASK": {
|
"RAG_CONSOLIDATION_TASK": {
|
||||||
"name": "RAG Consolidation",
|
"name": "RAG Consolidation",
|
||||||
"description": "A Task to consolidate questions and answers",
|
"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 common.utils.execution_progress import ExecutionProgressTracker
|
||||||
from eveai_api.api.auth import requires_service
|
from eveai_api.api.auth import requires_service
|
||||||
from common.models.interaction import Specialist
|
from common.models.interaction import Specialist
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
|
||||||
specialist_execution_ns = Namespace('specialist-execution', description='Specialist execution operations')
|
specialist_execution_ns = Namespace('specialist-execution', description='Specialist execution operations')
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ class StartSession(Resource):
|
|||||||
@requires_service("SPECIALIST_API")
|
@requires_service("SPECIALIST_API")
|
||||||
@specialist_execution_ns.response(201, 'New Session ID created Successfully', specialist_start_session_response)
|
@specialist_execution_ns.response(201, 'New Session ID created Successfully', specialist_start_session_response)
|
||||||
def get(self):
|
def get(self):
|
||||||
new_session_id = f"{uuid.uuid4()}"
|
new_session_id = SpecialistServices.start_session()
|
||||||
return {
|
return {
|
||||||
'session_id': new_session_id,
|
'session_id': new_session_id,
|
||||||
}, 201
|
}, 201
|
||||||
@@ -56,22 +57,16 @@ class StartExecution(Resource):
|
|||||||
data = specialist_execution_ns.payload
|
data = specialist_execution_ns.payload
|
||||||
|
|
||||||
# Send task to queue
|
# Send task to queue
|
||||||
task = current_celery.send_task(
|
result = SpecialistServices.execute_specialist(
|
||||||
'execute_specialist',
|
tenant_id=tenant_id,
|
||||||
args=[tenant_id,
|
specialist_id=data['specialist_id'],
|
||||||
data['specialist_id'],
|
specialist_arguments=data['arguments'],
|
||||||
data['arguments'],
|
session_id=data['session_id'],
|
||||||
data['session_id'],
|
user_timezone=data['user_timezone'])
|
||||||
data['user_timezone'],
|
|
||||||
],
|
|
||||||
queue='llm_interactions'
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
result['stream_url'] = f"/api/v1/specialist-execution/{result['task_id']}/stream"
|
||||||
'task_id': task.id,
|
|
||||||
'status': 'queued',
|
return result, 201
|
||||||
'stream_url': f'/api/v1/specialist-execution/{task.id}/stream'
|
|
||||||
}, 201
|
|
||||||
|
|
||||||
|
|
||||||
@specialist_execution_ns.route('/<string:task_id>/stream')
|
@specialist_execution_ns.route('/<string:task_id>/stream')
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import traceback
|
|||||||
import jinja2
|
import jinja2
|
||||||
from flask import render_template, request, jsonify, redirect, current_app, flash
|
from flask import render_template, request, jsonify, redirect, current_app, flash
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
|
from common.utils.eveai_exceptions import EveAINoSessionTenant
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +69,24 @@ def attribute_error_handler(error):
|
|||||||
error_details=error_msg), 500
|
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):
|
def general_exception(e):
|
||||||
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
|
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
|
||||||
flash('An application error occurred. The technical team has been notified.', 'error')
|
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(500, internal_server_error)
|
||||||
app.register_error_handler(401, not_authorised_error)
|
app.register_error_handler(401, not_authorised_error)
|
||||||
app.register_error_handler(403, 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(KeyError, key_error_handler)
|
||||||
app.register_error_handler(AttributeError, attribute_error_handler)
|
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||||
app.register_error_handler(Exception, general_exception)
|
app.register_error_handler(Exception, general_exception)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<div class="form-group mt-3 d-flex justify-content-between">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="action" value="view_chat_session" class="btn btn-primary" onclick="return validateTableSelection('chatSessionsForm')">View Chat Session</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ render_selectable_table(
|
{{ render_selectable_table(
|
||||||
headers=["Agent ID", "Name", "Type", "Status"],
|
headers=["Agent ID", "Name", "Type", "Type Version"],
|
||||||
rows=agent_rows if agent_rows else [],
|
rows=agent_rows if agent_rows else [],
|
||||||
selectable=True,
|
selectable=True,
|
||||||
id="agentsTable",
|
id="agentsTable",
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ render_selectable_table(
|
{{ render_selectable_table(
|
||||||
headers=["Task ID", "Name", "Type", "Status"],
|
headers=["Task ID", "Name", "Type", "Type Version"],
|
||||||
rows=task_rows if task_rows else [],
|
rows=task_rows if task_rows else [],
|
||||||
selectable=True,
|
selectable=True,
|
||||||
id="tasksTable",
|
id="tasksTable",
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ render_selectable_table(
|
{{ render_selectable_table(
|
||||||
headers=["Tool ID", "Name", "Type", "Status"],
|
headers=["Tool ID", "Name", "Type", "Type Version"],
|
||||||
rows=tool_rows if tool_rows else [],
|
rows=tool_rows if tool_rows else [],
|
||||||
selectable=True,
|
selectable=True,
|
||||||
id="toolsTable",
|
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 class="form-group mt-3 d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>
|
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>
|
||||||
|
<button type="submit" name="action" value="execute_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Execute Specialist</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" name="action" value="create_specialist" class="btn btn-success">Register Specialist</button>
|
<button type="submit" name="action" value="create_specialist" class="btn btn-success">Register Specialist</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,19 +50,18 @@
|
|||||||
{% if specialist_arguments %}
|
{% if specialist_arguments %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="mb-3">Specialist Arguments:</h6>
|
<h6 class="mb-3">Specialist Arguments:</h6>
|
||||||
<div class="code-wrapper">
|
<div id="args-viewer-{{ loop.index }}" class="json-viewer" style="height: 300px; width: 100%;"></div>
|
||||||
<pre><code class="language-json" style="width: 100%;">{{ specialist_arguments | tojson(indent=2) }}</code></pre>
|
<div id="args-viewer-{{ loop.index }}-data" class="d-none">{{ specialist_arguments | tojson(indent=2) }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<!-- Results Section -->
|
<!-- Results Section -->
|
||||||
{% if specialist_results %}
|
{% if specialist_results %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="mb-3">Specialist Results:</h6>
|
<h6 class="mb-3">Specialist Results:</h6>
|
||||||
<div class="code-wrapper">
|
<div id="results-viewer-{{ loop.index }}" class="json-viewer" style="height: 300px; width: 100%;"></div>
|
||||||
<pre><code class="language-json" style="width: 100%;">{{ specialist_results | tojson(indent=2) }}</code></pre>
|
<div id="results-viewer-{{ loop.index }}-data" class="d-none">{{ specialist_results | tojson(indent=2) }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -94,166 +93,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize syntax highlighting
|
// JSONEditor initialiseren wanneer een accordion item wordt geopend
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
const accordionButtons = document.querySelectorAll('.accordion-button');
|
||||||
hljs.highlightElement(block);
|
|
||||||
|
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>
|
</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>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_pagination(pagination, endpoint) %}
|
{#{% macro render_pagination(pagination, endpoint) %}#}
|
||||||
<nav aria-label="Page navigation">
|
{#<nav aria-label="Page navigation">#}
|
||||||
<ul class="pagination">
|
{# <ul class="pagination">#}
|
||||||
<!-- Previous Button -->
|
{# <!-- Previous Button -->#}
|
||||||
<li class="page-item {{ 'disabled' if not pagination.has_prev }}">
|
{# <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">
|
{# <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="material-symbols-outlined">keyboard_double_arrow_left</span>#}
|
||||||
{# <span class="sr-only">Previous</span>#}
|
{# </a>#}
|
||||||
</a>
|
{# </li>#}
|
||||||
</li>
|
{##}
|
||||||
|
{# <!-- Page Number Buttons -->#}
|
||||||
<!-- Page Number Buttons -->
|
{# {% for page in pagination.iter_pages(left_edge=1, left_current=2, right_current=3, right_edge=1) %}#}
|
||||||
{% 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 }}">#}
|
||||||
<li class="page-item {{ 'active' if page == pagination.page }}">
|
{# <a class="page-link" href="{{ url_for(endpoint, page=page) }}">#}
|
||||||
<a class="page-link" href="{{ url_for(endpoint, page=page) }}">
|
{# {% if page == pagination.page %}#}
|
||||||
{% if page == pagination.page %}
|
{# <span class="material-symbols-outlined">target</span>#}
|
||||||
<span class="material-symbols-outlined">target</span>
|
{# {% else %}#}
|
||||||
{# <span class="sr-only">(current)</span>#}
|
{# {{ page }}#}
|
||||||
{% else %}
|
{# {% endif %}#}
|
||||||
{{ page }}
|
{# </a>#}
|
||||||
{% endif %}
|
{# </li>#}
|
||||||
</a>
|
{# {% endfor %}#}
|
||||||
</li>
|
{##}
|
||||||
{% endfor %}
|
{# <!-- Next Button -->#}
|
||||||
|
{# <li class="page-item {{ 'disabled' if not pagination.has_next }}">#}
|
||||||
<!-- Next Button -->
|
{# <a class="page-link" href="{{ url_for(endpoint, page=pagination.next_num) if pagination.has_next else 'javascript:;' }}">#}
|
||||||
<li class="page-item {{ 'disabled' if not pagination.has_next }}">
|
{# <span class="material-symbols-outlined">keyboard_double_arrow_right</span>#}
|
||||||
<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>#}
|
{# <span class="sr-only">Next</span>#}
|
||||||
</a>
|
{# </a>#}
|
||||||
</li>
|
{# </li>#}
|
||||||
</ul>
|
{# </ul>#}
|
||||||
</nav>
|
{#</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 %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_filter_field(field_name, label, options, current_value) %}
|
{% macro render_filter_field(field_name, label, options, current_value) %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ field_name }}">{{ label }}</label>
|
<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">
|
<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 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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
@@ -156,6 +338,24 @@ function validateTableSelection(formId) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
<style>
|
||||||
.json-editor-container {
|
.json-editor-container {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ class DynamicFormBase(FlaskForm):
|
|||||||
message=f"Value must be between {min_value or '-∞'} and {max_value or '∞'}"
|
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':
|
elif field_type == 'tagging_fields':
|
||||||
validators_list.append(self._validate_tagging_fields)
|
validators_list.append(self._validate_tagging_fields)
|
||||||
elif field_type == 'tagging_fields_filter':
|
elif field_type == 'tagging_fields_filter':
|
||||||
@@ -130,6 +132,11 @@ class DynamicFormBase(FlaskForm):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(f"Invalid filter definition: {str(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):
|
def _validate_filter_condition(self, condition):
|
||||||
"""Recursively validate a filter condition structure"""
|
"""Recursively validate a filter condition structure"""
|
||||||
# Check if this is a logical condition (AND/OR/NOT)
|
# Check if this is a logical condition (AND/OR/NOT)
|
||||||
@@ -264,6 +271,7 @@ class DynamicFormBase(FlaskForm):
|
|||||||
'float': FloatField,
|
'float': FloatField,
|
||||||
'boolean': BooleanField,
|
'boolean': BooleanField,
|
||||||
'string': StringField,
|
'string': StringField,
|
||||||
|
'str': StringField,
|
||||||
'text': TextAreaField,
|
'text': TextAreaField,
|
||||||
'date': DateField,
|
'date': DateField,
|
||||||
'file': FileField,
|
'file': FileField,
|
||||||
@@ -368,6 +376,28 @@ class DynamicFormBase(FlaskForm):
|
|||||||
data[original_field_name] = field.data
|
data[original_field_name] = field.data
|
||||||
return 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):
|
def validate_tagging_fields(form, field):
|
||||||
"""Validate the tagging fields structure"""
|
"""Validate the tagging fields structure"""
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
|
from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
|
||||||
from wtforms.fields.datetime import DateField
|
from wtforms.fields.datetime import DateField
|
||||||
|
from wtforms.fields.numeric import IntegerField
|
||||||
from wtforms.validators import DataRequired, Length, Optional
|
from wtforms.validators import DataRequired, Length, Optional
|
||||||
|
|
||||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
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})
|
asset_type_version = StringField('Asset Type Version', validators=[DataRequired()], render_kw={'readonly': True})
|
||||||
bucket_name = StringField('Bucket Name', 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 ast
|
||||||
|
import json
|
||||||
from datetime import datetime as dt, timezone as tz
|
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 import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for
|
||||||
from flask_security import roles_accepted
|
from flask_security import roles_accepted
|
||||||
@@ -14,7 +16,9 @@ from common.models.interaction import (ChatSession, Interaction, InteractionEmbe
|
|||||||
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
|
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
|
||||||
|
|
||||||
from common.extensions import db, cache_manager
|
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.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.model_logging_utils import set_logging_information, update_logging_information
|
||||||
|
|
||||||
from common.utils.middleware import mw_before_request
|
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 config.type_defs.specialist_types import SPECIALIST_TYPES
|
||||||
|
|
||||||
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
||||||
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm)
|
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm)
|
||||||
|
|
||||||
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
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')
|
cs_id = ast.literal_eval(chat_session_identification).get('value')
|
||||||
|
|
||||||
action = request.form['action']
|
action = request.form['action']
|
||||||
|
current_app.logger.debug(f'Handle Chat Session Selection Action: {action}')
|
||||||
|
|
||||||
match action:
|
match action:
|
||||||
case 'view_chat_session':
|
case 'view_chat_session':
|
||||||
return redirect(prefixed_url_for('interaction_bp.view_chat_session', chat_session_id=cs_id))
|
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
|
# Add more conditions for other actions
|
||||||
return redirect(prefixed_url_for('interaction_bp.chat_sessions'))
|
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'])
|
@interaction_bp.route('/view_chat_session_by_session_id/<session_id>', methods=['GET'])
|
||||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
def view_chat_session_by_session_id(session_id):
|
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()
|
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):
|
def show_chat_session(chat_session):
|
||||||
@@ -303,6 +316,8 @@ def handle_specialist_selection():
|
|||||||
|
|
||||||
if action == "edit_specialist":
|
if action == "edit_specialist":
|
||||||
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=specialist_id))
|
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'))
|
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
||||||
|
|
||||||
@@ -391,9 +406,8 @@ def edit_tool(tool_id):
|
|||||||
form = EditEveAIToolForm(obj=tool)
|
form = EditEveAIToolForm(obj=tool)
|
||||||
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return render_template('interaction/components/edit_tool.html',
|
return render_template('interaction/components/edit_tool.html', form=form, tool=tool)
|
||||||
form=form,
|
return None
|
||||||
tool=tool)
|
|
||||||
|
|
||||||
|
|
||||||
@interaction_bp.route('/tool/<int:tool_id>/save', methods=['POST'])
|
@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)
|
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']
|
partner_id = session['partner']['id']
|
||||||
|
|
||||||
form = EditPartnerServiceForm(obj=partner_service)
|
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_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
|
||||||
partner_service.type_version)
|
partner_service.type_version)
|
||||||
configuration_config = partner_service_config.get('configuration')
|
configuration_config = partner_service_config.get('configuration')
|
||||||
@@ -174,11 +175,12 @@ def edit_partner_service(partner_service_id):
|
|||||||
f"{permissions_config}")
|
f"{permissions_config}")
|
||||||
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
|
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}")
|
current_app.logger.debug(f"Form returned: {form.data}")
|
||||||
raw_form_data = request.form.to_dict()
|
raw_form_data = request.form.to_dict()
|
||||||
current_app.logger.debug(f"Raw form data: {raw_form_data}")
|
current_app.logger.debug(f"Raw form data: {raw_form_data}")
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
form.populate_obj(partner_service)
|
form.populate_obj(partner_service)
|
||||||
partner_service.configuration = form.get_dynamic_data('configuration')
|
partner_service.configuration = form.get_dynamic_data('configuration')
|
||||||
partner_service.permissions = form.get_dynamic_data('permissions')
|
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']]
|
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
|
||||||
# initialise timezone
|
# initialise timezone
|
||||||
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
|
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
|
# Initialize fallback algorithms
|
||||||
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||||
# Show field only for Super Users with partner in session
|
# Show field only for Super Users with partner in session
|
||||||
|
|||||||
@@ -302,7 +302,6 @@ def handle_tenant_selection():
|
|||||||
# set tenant information in the session
|
# set tenant information in the session
|
||||||
session['tenant'] = the_tenant.to_dict()
|
session['tenant'] = the_tenant.to_dict()
|
||||||
session['default_language'] = the_tenant.default_language
|
session['default_language'] = the_tenant.default_language
|
||||||
session['llm_model'] = the_tenant.llm_model
|
|
||||||
# remove catalog-related items from the session
|
# remove catalog-related items from the session
|
||||||
session.pop('catalog_id', None)
|
session.pop('catalog_id', None)
|
||||||
session.pop('catalog_name', 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 typing import Dict, Any, List
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
from common.extensions import cache_manager
|
||||||
from common.models.interaction import SpecialistRetriever
|
from common.models.interaction import SpecialistRetriever
|
||||||
from common.utils.execution_progress import ExecutionProgressTracker
|
from common.utils.execution_progress import ExecutionProgressTracker
|
||||||
from config.logging_config import TuningLogger
|
from config.logging_config import TuningLogger
|
||||||
@@ -75,6 +76,8 @@ class BaseSpecialistExecutor(ABC):
|
|||||||
'tuning',
|
'tuning',
|
||||||
tenant_id=self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
specialist_id=self.specialist_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
|
# Verify logger is working with a test message
|
||||||
if self.tuning:
|
if self.tuning:
|
||||||
@@ -101,6 +104,13 @@ class BaseSpecialistExecutor(ABC):
|
|||||||
|
|
||||||
def get_specialist_class(specialist_type: str, type_version: str):
|
def get_specialist_class(specialist_type: str, type_version: str):
|
||||||
major_minor = '_'.join(type_version.split('.')[:2])
|
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}"
|
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)
|
module = importlib.import_module(module_path)
|
||||||
return module.SpecialistExecutor
|
return module.SpecialistExecutor
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ class EveAICrewAICrew(Crew):
|
|||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
def __init__(self, specialist, name: str, **kwargs):
|
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)
|
super().__init__(**kwargs)
|
||||||
self.specialist = specialist
|
self.specialist = specialist
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
|
|||||||
current_app.logger.error(f"Error detailing question: {e}")
|
current_app.logger.error(f"Error detailing question: {e}")
|
||||||
return question # Fallback to original question
|
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"):
|
with current_event.create_span("Specialist Retrieval"):
|
||||||
self.log_tuning("Starting context retrieval", {
|
self.log_tuning("Starting context retrieval", {
|
||||||
"num_retrievers": len(self.retrievers),
|
"num_retrievers": len(self.retrievers),
|
||||||
@@ -326,6 +326,8 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def execute_specialist(self, arguments: SpecialistArguments) -> SpecialistResult:
|
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
|
# Detail the incoming query
|
||||||
if self._cached_session.interactions:
|
if self._cached_session.interactions:
|
||||||
query = arguments.query
|
query = arguments.query
|
||||||
@@ -341,11 +343,12 @@ class CrewAIBaseSpecialistExecutor(BaseSpecialistExecutor):
|
|||||||
detailed_arguments = arguments.model_copy(update=modified_arguments)
|
detailed_arguments = arguments.model_copy(update=modified_arguments)
|
||||||
formatted_context, citations = self._retrieve_context(detailed_arguments)
|
formatted_context, citations = self._retrieve_context(detailed_arguments)
|
||||||
result = self.execute(detailed_arguments, formatted_context, citations)
|
result = self.execute(detailed_arguments, formatted_context, citations)
|
||||||
|
|
||||||
modified_result = {
|
modified_result = {
|
||||||
"detailed_query": detailed_query,
|
"detailed_query": detailed_query,
|
||||||
"citations": citations,
|
"citations": citations,
|
||||||
}
|
}
|
||||||
final_result = result.model_copy(update=modified_result)
|
final_result = result.model_copy(update=modified_result)
|
||||||
|
else:
|
||||||
|
final_result = self.execute(arguments, "", [])
|
||||||
|
|
||||||
return final_result
|
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.retrievers.retriever_typing import RetrieverArguments
|
||||||
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||||
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
|
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
|
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.retrievers.retriever_typing import RetrieverArguments
|
||||||
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||||
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
|
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.globals.identification.identification_v1_0 import LeadInfoOutput
|
||||||
from eveai_chat_workers.outputs.spin.spin_v1_0 import SPINOutput
|
from eveai_chat_workers.outputs.globals.spin.spin_v1_0 import SPINOutput
|
||||||
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
|
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
|
||||||
from common.utils.pydantic_utils import flatten_pydantic_model
|
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(
|
current_app.logger.info(
|
||||||
f'execute_specialist: Processing request for tenant {tenant_id} using specialist {specialist_id}')
|
f'execute_specialist: Processing request for tenant {tenant_id} using specialist {specialist_id}')
|
||||||
|
|
||||||
|
new_interaction = None
|
||||||
try:
|
try:
|
||||||
# Ensure we have a session
|
# Ensure we have a session
|
||||||
cached_session = cache_manager.chat_session_cache.get_cached_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:
|
except Exception as e:
|
||||||
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
|
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
|
||||||
current_app.logger.error(f'execute_specialist: Error executing specialist: {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
|
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;
|
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
|
langcodes~=3.4.0
|
||||||
langdetect~=1.0.9
|
langdetect~=1.0.9
|
||||||
langsmith~=0.1.81
|
langsmith~=0.1.81
|
||||||
openai~=1.77.0
|
openai~=1.75.0
|
||||||
pg8000~=1.31.2
|
pg8000~=1.31.2
|
||||||
pgvector~=0.2.5
|
pgvector~=0.2.5
|
||||||
pycryptodome~=3.20.0
|
pycryptodome~=3.20.0
|
||||||
@@ -82,11 +82,11 @@ typing_extensions~=4.12.2
|
|||||||
babel~=2.16.0
|
babel~=2.16.0
|
||||||
dogpile.cache~=1.3.3
|
dogpile.cache~=1.3.3
|
||||||
python-docx~=1.1.2
|
python-docx~=1.1.2
|
||||||
crewai~=0.118.0
|
crewai~=0.121.0
|
||||||
sseclient~=0.0.27
|
sseclient~=0.0.27
|
||||||
termcolor~=2.5.0
|
termcolor~=2.5.0
|
||||||
mistral-common~=1.5.3
|
mistral-common~=1.5.5
|
||||||
mistralai~=1.6.0
|
mistralai~=1.7.1
|
||||||
contextvars~=2.4
|
contextvars~=2.4
|
||||||
pandas~=2.2.3
|
pandas~=2.2.3
|
||||||
prometheus_client~=0.21.1
|
prometheus_client~=0.21.1
|
||||||
|
|||||||
Reference in New Issue
Block a user