diff --git a/common/models/interaction.py b/common/models/interaction.py index f37c04f..24f9ce9 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -8,7 +8,7 @@ from .document import Embedding, Retriever class ChatSession(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True) - session_id = db.Column(db.String(36), nullable=True) + session_id = db.Column(db.String(49), nullable=True) session_start = db.Column(db.DateTime, nullable=False) session_end = db.Column(db.DateTime, nullable=True) timezone = db.Column(db.String(30), nullable=True) @@ -189,6 +189,7 @@ class Interaction(db.Model): question_at = db.Column(db.DateTime, nullable=False) detailed_question_at = db.Column(db.DateTime, nullable=True) answer_at = db.Column(db.DateTime, nullable=True) + processing_error = db.Column(db.String(255), nullable=True) # Relations embeddings = db.relationship('InteractionEmbedding', backref='interaction', lazy=True) diff --git a/common/services/interaction/specialist_services.py b/common/services/interaction/specialist_services.py new file mode 100644 index 0000000..8bc0a10 --- /dev/null +++ b/common/services/interaction/specialist_services.py @@ -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', + } + + diff --git a/common/utils/cache/config_cache.py b/common/utils/cache/config_cache.py index e101a89..f923c00 100644 --- a/common/utils/cache/config_cache.py +++ b/common/utils/cache/config_cache.py @@ -135,7 +135,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]): Checks both global and partner-specific directories """ # First check the global path - global_path = Path(self._config_dir) / "global" / type_name + global_path = Path(self._config_dir) / "globals" / type_name # If global path doesn't exist, check if the type exists directly in the root # (for backward compatibility) @@ -145,7 +145,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]): if not global_path.exists(): # Check if it exists in any partner subdirectories partner_dirs = [d for d in Path(self._config_dir).iterdir() - if d.is_dir() and d.name != "global"] + if d.is_dir() and d.name != "globals"] for partner_dir in partner_dirs: partner_type_path = partner_dir / type_name @@ -178,7 +178,7 @@ class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]): metadata = yaml_data.get('metadata', {}) # Add partner information if available partner = None - if "global" not in str(file_path): + if "globals" not in str(file_path): # Extract partner name from path # Path format: config_dir/partner_name/type_name/version.yaml partner = file_path.parent.parent.name diff --git a/common/utils/template_filters.py b/common/utils/template_filters.py index 4527ef1..07034ee 100644 --- a/common/utils/template_filters.py +++ b/common/utils/template_filters.py @@ -5,6 +5,7 @@ import markdown from markupsafe import Markup from datetime import datetime from common.utils.nginx_utils import prefixed_url_for as puf +from flask import current_app, url_for def to_local_time(utc_dt, timezone_str): @@ -83,8 +84,27 @@ def clean_markdown(text): return text -def prefixed_url_for(endpoint): - return puf(endpoint) +def prefixed_url_for(endpoint, **kwargs): + return puf(endpoint, **kwargs) + + +def get_pagination_html(pagination, endpoint, **kwargs): + """ + Generates HTML for pagination with the ability to include additional parameters + """ + html = ['') + return Markup(''.join(html)) def register_filters(app): @@ -99,3 +119,5 @@ def register_filters(app): app.jinja_env.filters['clean_markdown'] = clean_markdown app.jinja_env.globals['prefixed_url_for'] = prefixed_url_for + app.jinja_env.globals['get_pagination_html'] = get_pagination_html + diff --git a/config/agents/global/EMAIL_CONTENT_AGENT/1.0.0.yaml b/config/agents/globals/EMAIL_CONTENT_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/EMAIL_CONTENT_AGENT/1.0.0.yaml rename to config/agents/globals/EMAIL_CONTENT_AGENT/1.0.0.yaml diff --git a/config/agents/global/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml b/config/agents/globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml rename to config/agents/globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml diff --git a/config/agents/global/IDENTIFICATION_AGENT/1.0.0.yaml b/config/agents/globals/IDENTIFICATION_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/IDENTIFICATION_AGENT/1.0.0.yaml rename to config/agents/globals/IDENTIFICATION_AGENT/1.0.0.yaml diff --git a/config/agents/global/RAG_AGENT/1.0.0.yaml b/config/agents/globals/RAG_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/RAG_AGENT/1.0.0.yaml rename to config/agents/globals/RAG_AGENT/1.0.0.yaml diff --git a/config/agents/global/RAG_COMMUNICATION_AGENT/1.0.0.yaml b/config/agents/globals/RAG_COMMUNICATION_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/RAG_COMMUNICATION_AGENT/1.0.0.yaml rename to config/agents/globals/RAG_COMMUNICATION_AGENT/1.0.0.yaml diff --git a/config/agents/global/SPIN_DETECTION_AGENT/1.0.0.yaml b/config/agents/globals/SPIN_DETECTION_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/SPIN_DETECTION_AGENT/1.0.0.yaml rename to config/agents/globals/SPIN_DETECTION_AGENT/1.0.0.yaml diff --git a/config/agents/global/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml b/config/agents/globals/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml similarity index 100% rename from config/agents/global/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml rename to config/agents/globals/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml diff --git a/config/agents/traicie/TRAICIE_HR_BP_AGENT/1.0.0.yaml b/config/agents/traicie/TRAICIE_HR_BP_AGENT/1.0.0.yaml new file mode 100644 index 0000000..a334c29 --- /dev/null +++ b/config/agents/traicie/TRAICIE_HR_BP_AGENT/1.0.0.yaml @@ -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" diff --git a/config/assets/global/DOCUMENT_TEMPLATE/1.0.0.yaml b/config/assets/globals/DOCUMENT_TEMPLATE/1.0.0.yaml similarity index 100% rename from config/assets/global/DOCUMENT_TEMPLATE/1.0.0.yaml rename to config/assets/globals/DOCUMENT_TEMPLATE/1.0.0.yaml diff --git a/config/assets/globals/SPECIALIST_CONFIGURATION/1.0.0.yaml b/config/assets/globals/SPECIALIST_CONFIGURATION/1.0.0.yaml new file mode 100644 index 0000000..66da3e0 --- /dev/null +++ b/config/assets/globals/SPECIALIST_CONFIGURATION/1.0.0.yaml @@ -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" diff --git a/config/logging_config.py b/config/logging_config.py index fdecf3d..3b74424 100644 --- a/config/logging_config.py +++ b/config/logging_config.py @@ -47,6 +47,7 @@ class TuningLogRecord(logging.LogRecord): self._tuning_specialist_id = None self._tuning_retriever_id = None self._tuning_processor_id = None + self._session_id = None self.component = os.environ.get('COMPONENT_NAME', 'eveai_app') def getMessage(self): @@ -87,16 +88,18 @@ class TuningLogRecord(logging.LogRecord): 'tuning_specialist_id': self._tuning_specialist_id, 'tuning_retriever_id': self._tuning_retriever_id, 'tuning_processor_id': self._tuning_processor_id, + 'session_id': self._session_id, } def set_tuning_data(self, tenant_id=None, catalog_id=None, specialist_id=None, - retriever_id=None, processor_id=None): + retriever_id=None, processor_id=None, session_id=None,): """Set tuning-specific data""" object.__setattr__(self, '_tuning_tenant_id', tenant_id) object.__setattr__(self, '_tuning_catalog_id', catalog_id) object.__setattr__(self, '_tuning_specialist_id', specialist_id) object.__setattr__(self, '_tuning_retriever_id', retriever_id) object.__setattr__(self, '_tuning_processor_id', processor_id) + object.__setattr__(self, '_session_id', session_id) class TuningFormatter(logging.Formatter): @@ -120,6 +123,12 @@ class TuningFormatter(logging.Formatter): identifiers.append(f"Catalog: {record.catalog_id}") if hasattr(record, 'processor_id') and record.processor_id: identifiers.append(f"Processor: {record.processor_id}") + if hasattr(record, 'specialist_id') and record.specialist_id: + identifiers.append(f"Specialist: {record.specialist_id}") + if hasattr(record, 'retriever_id') and record.retriever_id: + identifiers.append(f"Retriever: {record.retriever_id}") + if hasattr(record, 'session_id') and record.session_id: + identifiers.append(f"Session: {record.session_id}") formatted_msg = ( f"{formatted_msg}\n" @@ -149,22 +158,93 @@ class GraylogFormatter(logging.Formatter): 'specialist_id': record.specialist_id, 'retriever_id': record.retriever_id, 'processor_id': record.processor_id, + 'session_id': record.session_id, } return super().format(record) - class TuningLogger: """Helper class to manage tuning logs with consistent structure""" - def __init__(self, logger_name, tenant_id=None, catalog_id=None, specialist_id=None, retriever_id=None, processor_id=None): + def __init__(self, logger_name, tenant_id=None, catalog_id=None, specialist_id=None, retriever_id=None, + processor_id=None, session_id=None, log_file=None): + """ + Initialize a tuning logger + + Args: + logger_name: Base name for the logger + tenant_id: Optional tenant ID for context + catalog_id: Optional catalog ID for context + specialist_id: Optional specialist ID for context + retriever_id: Optional retriever ID for context + processor_id: Optional processor ID for context + session_id: Optional session ID for context and log file naming + log_file: Optional custom log file name to use + """ + self.logger = logging.getLogger(logger_name) self.tenant_id = tenant_id self.catalog_id = catalog_id self.specialist_id = specialist_id self.retriever_id = retriever_id self.processor_id = processor_id + self.session_id = session_id + self.log_file = log_file + # Determine whether to use a session-specific logger + if session_id: + # Create a unique logger name for this session + session_logger_name = f"{logger_name}_{session_id}" + self.logger = logging.getLogger(session_logger_name) - def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DEBUG): + # If this logger doesn't have handlers yet, configure it + if not self.logger.handlers: + # Determine log file path + if not log_file and session_id: + log_file = f"logs/tuning_{session_id}.log" + elif not log_file: + log_file = "logs/tuning.log" + + # Configure the logger + self._configure_session_logger(log_file) + else: + # Use the standard tuning logger + self.logger = logging.getLogger(logger_name) + + def _configure_session_logger(self, log_file): + """Configure a new session-specific logger with appropriate handlers""" + # Create and configure a file handler + file_handler = logging.handlers.RotatingFileHandler( + filename=log_file, + maxBytes=1024 * 1024 * 3, # 3MB + backupCount=3 + ) + file_handler.setFormatter(TuningFormatter()) + file_handler.setLevel(logging.DEBUG) + + # Add the file handler to the logger + self.logger.addHandler(file_handler) + + # Add Graylog handler in production + env = os.environ.get('FLASK_ENV', 'development') + if env == 'production': + try: + graylog_handler = GELFUDPHandler( + host=GRAYLOG_HOST, + port=GRAYLOG_PORT, + debugging_fields=True + ) + graylog_handler.setFormatter(GraylogFormatter()) + self.logger.addHandler(graylog_handler) + except Exception as e: + # Fall back to just file logging if Graylog setup fails + fallback_logger = logging.getLogger('eveai_app') + fallback_logger.warning(f"Failed to set up Graylog handler: {str(e)}") + + # Set logger level and disable propagation + self.logger.setLevel(logging.DEBUG) + self.logger.propagate = False + + +def log_tuning(self, tuning_type: str, message: str, data=None, level=logging.DEBUG): """Log a tuning event with structured data""" try: # Create a standard LogRecord for tuning @@ -186,6 +266,7 @@ class TuningLogger: record.specialist_id = self.specialist_id record.retriever_id = self.retriever_id record.processor_id = self.processor_id + record.session_id = self.session_id if data: record.tuning_data = data diff --git a/config/partner_services/global/MANAGEMENT_SERVICE/1.0.0.yaml b/config/partner_services/globals/MANAGEMENT_SERVICE/1.0.0.yaml similarity index 100% rename from config/partner_services/global/MANAGEMENT_SERVICE/1.0.0.yaml rename to config/partner_services/globals/MANAGEMENT_SERVICE/1.0.0.yaml diff --git a/config/partner_services/global/SPECIALIST_SERVICE/1.0.0.yaml b/config/partner_services/globals/SPECIALIST_SERVICE/1.0.0.yaml similarity index 50% rename from config/partner_services/global/SPECIALIST_SERVICE/1.0.0.yaml rename to config/partner_services/globals/SPECIALIST_SERVICE/1.0.0.yaml index e881527..25fd0f0 100644 --- a/config/partner_services/global/SPECIALIST_SERVICE/1.0.0.yaml +++ b/config/partner_services/globals/SPECIALIST_SERVICE/1.0.0.yaml @@ -1,6 +1,11 @@ version: "1.0.0" name: "Management Service" -configuration: {} +configuration: + specialist_denominator: + name: "Specialist Denominator" + type: "string" + description: "Name defining the denominator for the specialist. Needs to be unique." + required: False permissions: {} metadata: author: "Josako" diff --git a/config/prompts/global/encyclopedia/1.0.0.yaml b/config/prompts/globals/encyclopedia/1.0.0.yaml similarity index 100% rename from config/prompts/global/encyclopedia/1.0.0.yaml rename to config/prompts/globals/encyclopedia/1.0.0.yaml diff --git a/config/prompts/global/history/1.0.0.yaml b/config/prompts/globals/history/1.0.0.yaml similarity index 100% rename from config/prompts/global/history/1.0.0.yaml rename to config/prompts/globals/history/1.0.0.yaml diff --git a/config/prompts/global/html_parse/1.0.0.yaml b/config/prompts/globals/html_parse/1.0.0.yaml similarity index 100% rename from config/prompts/global/html_parse/1.0.0.yaml rename to config/prompts/globals/html_parse/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/encyclopedia/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/encyclopedia/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/encyclopedia/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/encyclopedia/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/history/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/history/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/history/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/history/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/html_parse/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/html_parse/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/html_parse/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/html_parse/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/pdf_parse/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/pdf_parse/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/pdf_parse/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/pdf_parse/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/rag/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/rag/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/rag/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/rag/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/summary/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/summary/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/summary/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/summary/1.0.0.yaml diff --git a/config/prompts/global/openai/gpt-4o/transcript/1.0.0.yaml b/config/prompts/globals/openai/gpt-4o/transcript/1.0.0.yaml similarity index 100% rename from config/prompts/global/openai/gpt-4o/transcript/1.0.0.yaml rename to config/prompts/globals/openai/gpt-4o/transcript/1.0.0.yaml diff --git a/config/prompts/global/pdf_parse/1.0.0.yaml b/config/prompts/globals/pdf_parse/1.0.0.yaml similarity index 100% rename from config/prompts/global/pdf_parse/1.0.0.yaml rename to config/prompts/globals/pdf_parse/1.0.0.yaml diff --git a/config/prompts/global/rag/1.0.0.yaml b/config/prompts/globals/rag/1.0.0.yaml similarity index 100% rename from config/prompts/global/rag/1.0.0.yaml rename to config/prompts/globals/rag/1.0.0.yaml diff --git a/config/prompts/global/summary/1.0.0.yaml b/config/prompts/globals/summary/1.0.0.yaml similarity index 100% rename from config/prompts/global/summary/1.0.0.yaml rename to config/prompts/globals/summary/1.0.0.yaml diff --git a/config/prompts/global/transcript/1.0.0.yaml b/config/prompts/globals/transcript/1.0.0.yaml similarity index 100% rename from config/prompts/global/transcript/1.0.0.yaml rename to config/prompts/globals/transcript/1.0.0.yaml diff --git a/config/retrievers/global/DOSSIER_RETRIEVER/1.0.0.yaml b/config/retrievers/globals/DOSSIER_RETRIEVER/1.0.0.yaml similarity index 100% rename from config/retrievers/global/DOSSIER_RETRIEVER/1.0.0.yaml rename to config/retrievers/globals/DOSSIER_RETRIEVER/1.0.0.yaml diff --git a/config/retrievers/global/STANDARD_RAG/1.0.0.yaml b/config/retrievers/globals/STANDARD_RAG/1.0.0.yaml similarity index 100% rename from config/retrievers/global/STANDARD_RAG/1.0.0.yaml rename to config/retrievers/globals/STANDARD_RAG/1.0.0.yaml diff --git a/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0.yaml b/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0.yaml deleted file mode 100644 index e362c12..0000000 --- a/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0.yaml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/config/specialists/global/RAG_SPECIALIST/1.0.0.yaml b/config/specialists/globals/RAG_SPECIALIST/1.0.0.yaml similarity index 99% rename from config/specialists/global/RAG_SPECIALIST/1.0.0.yaml rename to config/specialists/globals/RAG_SPECIALIST/1.0.0.yaml index b068790..33117e7 100644 --- a/config/specialists/global/RAG_SPECIALIST/1.0.0.yaml +++ b/config/specialists/globals/RAG_SPECIALIST/1.0.0.yaml @@ -1,6 +1,7 @@ version: "1.0.0" name: "RAG Specialist" framework: "crewai" +chat: true configuration: name: name: "name" diff --git a/config/specialists/global/SPIN_SPECIALIST/1.0.0.yaml b/config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml similarity index 99% rename from config/specialists/global/SPIN_SPECIALIST/1.0.0.yaml rename to config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml index 891cd54..8fcc113 100644 --- a/config/specialists/global/SPIN_SPECIALIST/1.0.0.yaml +++ b/config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml @@ -1,6 +1,7 @@ version: "1.0.0" name: "Spin Sales Specialist" framework: "crewai" +chat: true configuration: name: name: "name" diff --git a/config/specialists/global/SPIN_SPECIALIST/1.0.0_overview.svg b/config/specialists/globals/SPIN_SPECIALIST/1.0.0_overview.svg similarity index 100% rename from config/specialists/global/SPIN_SPECIALIST/1.0.0_overview.svg rename to config/specialists/globals/SPIN_SPECIALIST/1.0.0_overview.svg diff --git a/config/specialists/global/STANDARD_RAG_SPECIALIST/1.0.0.yaml b/config/specialists/globals/STANDARD_RAG_SPECIALIST/1.0.0.yaml similarity index 99% rename from config/specialists/global/STANDARD_RAG_SPECIALIST/1.0.0.yaml rename to config/specialists/globals/STANDARD_RAG_SPECIALIST/1.0.0.yaml index 1fe475a..5c5c1c9 100644 --- a/config/specialists/global/STANDARD_RAG_SPECIALIST/1.0.0.yaml +++ b/config/specialists/globals/STANDARD_RAG_SPECIALIST/1.0.0.yaml @@ -1,6 +1,7 @@ version: 1.0.0 name: "Standard RAG Specialist" framework: "langchain" +chat: true configuration: specialist_context: name: "Specialist Context" diff --git a/config/specialists/traicie/TRAICIE_VACANCY_DEFINITION_SPECIALIST/1.0.0.yaml b/config/specialists/traicie/TRAICIE_VACANCY_DEFINITION_SPECIALIST/1.0.0.yaml new file mode 100644 index 0000000..9893360 --- /dev/null +++ b/config/specialists/traicie/TRAICIE_VACANCY_DEFINITION_SPECIALIST/1.0.0.yaml @@ -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" \ No newline at end of file diff --git a/config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0_overview.svg b/config/specialists/traicie/TRAICIE_VACANCY_DEFINITION_SPECIALIST/1.0.0_overview.svg similarity index 100% rename from config/specialists/Traicie/TRAICIE_VACATURE_SPECIALIST/1.0.0_overview.svg rename to config/specialists/traicie/TRAICIE_VACANCY_DEFINITION_SPECIALIST/1.0.0_overview.svg diff --git a/config/tasks/global/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml b/config/tasks/globals/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml rename to config/tasks/globals/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml diff --git a/config/tasks/global/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml b/config/tasks/globals/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml rename to config/tasks/globals/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml diff --git a/config/tasks/global/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml b/config/tasks/globals/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml rename to config/tasks/globals/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml diff --git a/config/tasks/global/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/globals/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml rename to config/tasks/globals/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml diff --git a/config/tasks/global/RAG_CONSOLIDATION_TASK/1.0.0.yaml b/config/tasks/globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/RAG_CONSOLIDATION_TASK/1.0.0.yaml rename to config/tasks/globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml diff --git a/config/tasks/global/RAG_TASK/1.0.0.yaml b/config/tasks/globals/RAG_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/RAG_TASK/1.0.0.yaml rename to config/tasks/globals/RAG_TASK/1.0.0.yaml diff --git a/config/tasks/global/SPIN_DETECT_TASK/1.0.0.yaml b/config/tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/SPIN_DETECT_TASK/1.0.0.yaml rename to config/tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml diff --git a/config/tasks/global/SPIN_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/globals/SPIN_QUESTIONS_TASK/1.0.0.yaml similarity index 100% rename from config/tasks/global/SPIN_QUESTIONS_TASK/1.0.0.yaml rename to config/tasks/globals/SPIN_QUESTIONS_TASK/1.0.0.yaml diff --git a/config/tasks/traicie/TRAICIE_GET_COMPETENCIES_TASK/1.0.0.yaml b/config/tasks/traicie/TRAICIE_GET_COMPETENCIES_TASK/1.0.0.yaml new file mode 100644 index 0000000..7838961 --- /dev/null +++ b/config/tasks/traicie/TRAICIE_GET_COMPETENCIES_TASK/1.0.0.yaml @@ -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" diff --git a/config/tasks/traicie/TRAICIE_GET_KO_CRITERIA_TASK/1.0.0.yaml b/config/tasks/traicie/TRAICIE_GET_KO_CRITERIA_TASK/1.0.0.yaml new file mode 100644 index 0000000..54194e6 --- /dev/null +++ b/config/tasks/traicie/TRAICIE_GET_KO_CRITERIA_TASK/1.0.0.yaml @@ -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" diff --git a/config/type_defs/agent_types.py b/config/type_defs/agent_types.py index 934af36..9a6352b 100644 --- a/config/type_defs/agent_types.py +++ b/config/type_defs/agent_types.py @@ -28,4 +28,9 @@ AGENT_TYPES = { "name": "SPIN Sales Specialist", "description": "An Agent that asks for Follow-up questions for SPIN-process", }, + "TRAICIE_HR_BP_AGENT": { + "name": "Traicie HR BP Agent", + "description": "An HR Business Partner Agent", + "partner": "traicie" + } } diff --git a/config/type_defs/asset_types.py b/config/type_defs/asset_types.py index 41c769d..9408a27 100644 --- a/config/type_defs/asset_types.py +++ b/config/type_defs/asset_types.py @@ -4,4 +4,8 @@ AGENT_TYPES = { "name": "Document Template", "description": "Asset that defines a template in markdown a specialist can process", }, + "SPECIALIST_CONFIGURATION": { + "name": "Specialist Configuration", + "description": "Asset that defines a specialist configuration", + }, } diff --git a/config/type_defs/specialist_types.py b/config/type_defs/specialist_types.py index 4d002f2..526f90f 100644 --- a/config/type_defs/specialist_types.py +++ b/config/type_defs/specialist_types.py @@ -12,9 +12,9 @@ SPECIALIST_TYPES = { "name": "Spin Sales Specialist", "description": "A specialist that allows to answer user queries, try to get SPIN-information and Identification", }, - "TRAICIE_VACATURE_SPECIALIST": { - "name": "Traicie Vacature Specialist", - "description": "Specialist configureerbaar voor een specifieke vacature", - "partner": "Traicie" + "TRAICIE_VACANCY_DEFINITION_SPECIALIST": { + "name": "Traicie Vacancy Definition Specialist", + "description": "Assistant to create a new Vacancy based on Vacancy Text", + "partner": "traicie" } } \ No newline at end of file diff --git a/config/type_defs/task_types.py b/config/type_defs/task_types.py index 9f43d46..612ccfa 100644 --- a/config/type_defs/task_types.py +++ b/config/type_defs/task_types.py @@ -31,5 +31,15 @@ TASK_TYPES = { "RAG_CONSOLIDATION_TASK": { "name": "RAG Consolidation", "description": "A Task to consolidate questions and answers", + }, + "TRAICIE_GET_COMPETENCIES_TASK": { + "name": "Traicie Get Competencies", + "description": "A Task to get Competencies from a Vacancy Text", + "partner": "traicie" + }, + "TRAICIE_GET_KO_CRITERIA_TASK": { + "name": "Traicie Get KO Criteria", + "description": "A Task to get KO Criteria from a Vacancy Text", + "partner": "traicie" } } diff --git a/eveai_api/api/specialist_execution_api.py b/eveai_api/api/specialist_execution_api.py index 11f88e1..34a6e69 100644 --- a/eveai_api/api/specialist_execution_api.py +++ b/eveai_api/api/specialist_execution_api.py @@ -10,6 +10,7 @@ from common.utils.celery_utils import current_celery from common.utils.execution_progress import ExecutionProgressTracker from eveai_api.api.auth import requires_service from common.models.interaction import Specialist +from common.services.interaction.specialist_services import SpecialistServices specialist_execution_ns = Namespace('specialist-execution', description='Specialist execution operations') @@ -24,7 +25,7 @@ class StartSession(Resource): @requires_service("SPECIALIST_API") @specialist_execution_ns.response(201, 'New Session ID created Successfully', specialist_start_session_response) def get(self): - new_session_id = f"{uuid.uuid4()}" + new_session_id = SpecialistServices.start_session() return { 'session_id': new_session_id, }, 201 @@ -56,22 +57,16 @@ class StartExecution(Resource): data = specialist_execution_ns.payload # Send task to queue - task = current_celery.send_task( - 'execute_specialist', - args=[tenant_id, - data['specialist_id'], - data['arguments'], - data['session_id'], - data['user_timezone'], - ], - queue='llm_interactions' - ) + result = SpecialistServices.execute_specialist( + tenant_id=tenant_id, + specialist_id=data['specialist_id'], + specialist_arguments=data['arguments'], + session_id=data['session_id'], + user_timezone=data['user_timezone']) - return { - 'task_id': task.id, - 'status': 'queued', - 'stream_url': f'/api/v1/specialist-execution/{task.id}/stream' - }, 201 + result['stream_url'] = f"/api/v1/specialist-execution/{result['task_id']}/stream" + + return result, 201 @specialist_execution_ns.route('//stream') diff --git a/eveai_app/errors.py b/eveai_app/errors.py index 4e8ae53..aba1260 100644 --- a/eveai_app/errors.py +++ b/eveai_app/errors.py @@ -3,6 +3,8 @@ import traceback import jinja2 from flask import render_template, request, jsonify, redirect, current_app, flash from flask_login import current_user + +from common.utils.eveai_exceptions import EveAINoSessionTenant from common.utils.nginx_utils import prefixed_url_for @@ -67,6 +69,24 @@ def attribute_error_handler(error): error_details=error_msg), 500 +def no_tenant_selected_error(error): + """Handle errors when no tenant is selected in the current session. + + This typically happens when a session expires or becomes invalid after + a long period of inactivity. The user will be redirected to the login page. + """ + current_app.logger.error(f"No Session Tenant Error: {error}") + flash('Your session expired. You will have to re-enter your credentials', 'warning') + + # Perform logout if user is authenticated + if current_user.is_authenticated: + from flask_security.utils import logout_user + logout_user() + + # Redirect to login page + return redirect(prefixed_url_for('security.login')) + + def general_exception(e): current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True) flash('An application error occurred. The technical team has been notified.', 'error') @@ -80,6 +100,7 @@ def register_error_handlers(app): app.register_error_handler(500, internal_server_error) app.register_error_handler(401, not_authorised_error) app.register_error_handler(403, not_authorised_error) + app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error) app.register_error_handler(KeyError, key_error_handler) app.register_error_handler(AttributeError, attribute_error_handler) app.register_error_handler(Exception, general_exception) diff --git a/eveai_app/templates/interaction/chat_sessions.html b/eveai_app/templates/interaction/chat_sessions.html index 304643d..cbdc5c3 100644 --- a/eveai_app/templates/interaction/chat_sessions.html +++ b/eveai_app/templates/interaction/chat_sessions.html @@ -14,6 +14,7 @@
+
diff --git a/eveai_app/templates/interaction/edit_specialist.html b/eveai_app/templates/interaction/edit_specialist.html index 553c433..f4dab2b 100644 --- a/eveai_app/templates/interaction/edit_specialist.html +++ b/eveai_app/templates/interaction/edit_specialist.html @@ -84,7 +84,7 @@
{{ render_selectable_table( - headers=["Agent ID", "Name", "Type", "Status"], + headers=["Agent ID", "Name", "Type", "Type Version"], rows=agent_rows if agent_rows else [], selectable=True, id="agentsTable", @@ -105,7 +105,7 @@
{{ render_selectable_table( - headers=["Task ID", "Name", "Type", "Status"], + headers=["Task ID", "Name", "Type", "Type Version"], rows=task_rows if task_rows else [], selectable=True, id="tasksTable", @@ -126,7 +126,7 @@
{{ render_selectable_table( - headers=["Tool ID", "Name", "Type", "Status"], + headers=["Tool ID", "Name", "Type", "Type Version"], rows=tool_rows if tool_rows else [], selectable=True, id="toolsTable", diff --git a/eveai_app/templates/interaction/execute_specialist.html b/eveai_app/templates/interaction/execute_specialist.html new file mode 100644 index 0000000..e797162 --- /dev/null +++ b/eveai_app/templates/interaction/execute_specialist.html @@ -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.hidden_tag() }} + {% set disabled_fields = [] %} + {% set exclude_fields = [] %} + {% for field in form %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + +
+{% endblock %} + +{% block content_footer %} + +{% endblock %} diff --git a/eveai_app/templates/interaction/session_interactions.html b/eveai_app/templates/interaction/session_interactions.html new file mode 100644 index 0000000..9c5a3c0 --- /dev/null +++ b/eveai_app/templates/interaction/session_interactions.html @@ -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 %}
{% endblock %} + +{% block content %} +
+
+ {{ render_selectable_table(headers=["ID", "Question At", "Detailed Question At", "Answer At", "Processing Error"], rows=rows, selectable=False, id="interactionsTable") }} +{#
#} +{#
#} +{# #} +{#
#} +{#
#} +
+
+{% endblock %} + +{% block content_footer %} + {{ get_pagination_html(pagination, 'interaction_bp.session_interactions_by_session_id', session_id=chat_session.session_id) }} +{% endblock %} diff --git a/eveai_app/templates/interaction/specialist_execution_status.html b/eveai_app/templates/interaction/specialist_execution_status.html new file mode 100644 index 0000000..7d8cf28 --- /dev/null +++ b/eveai_app/templates/interaction/specialist_execution_status.html @@ -0,0 +1,192 @@ +{% extends "base.html" %} + +{% block title %}Specialist Execution Status{% endblock %} + +{% block content %} +
+
+
+
+
+

Specialist Execution Status

+ {% if specialist %} + {{ specialist.name }} + {% endif %} +
+
+
+
+
+ +
+
Current Status:
+
Execution starting...
+
+ +
+
Execution Log:
+
+
Waiting for updates...
+
+
+ +
+
+
Execution Completed
+

The specialist has completed processing.

+ View Results + Back to Specialists +
+ +
+
+
Error Occurred
+

An error occurred during specialist execution.

+
+ Back to Specialists +
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/interaction/specialists.html b/eveai_app/templates/interaction/specialists.html index 0e22d0d..d8dff02 100644 --- a/eveai_app/templates/interaction/specialists.html +++ b/eveai_app/templates/interaction/specialists.html @@ -14,6 +14,7 @@
+
diff --git a/eveai_app/templates/interaction/view_chat_session.html b/eveai_app/templates/interaction/view_chat_session.html index 53c22ef..39b8bd7 100644 --- a/eveai_app/templates/interaction/view_chat_session.html +++ b/eveai_app/templates/interaction/view_chat_session.html @@ -50,19 +50,18 @@ {% if specialist_arguments %}
Specialist Arguments:
-
-
{{ specialist_arguments | tojson(indent=2) }}
-
+
+
{{ specialist_arguments | tojson(indent=2) }}
{% endif %} + {% if specialist_results %}
Specialist Results:
-
-
{{ specialist_results | tojson(indent=2) }}
-
+
+
{{ specialist_results | tojson(indent=2) }}
{% endif %} @@ -94,167 +93,42 @@
{% endblock %} - -{% block styles %} -{{ super() }} - -{% endblock %} - {% block scripts %} -{{ super() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/eveai_app/templates/interaction/waiting_for_session.html b/eveai_app/templates/interaction/waiting_for_session.html new file mode 100644 index 0000000..fc095ee --- /dev/null +++ b/eveai_app/templates/interaction/waiting_for_session.html @@ -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 %}
{% endblock %} + +{% block extra_head %} + + +{% endblock %} + +{% block content %} +
+
+
+

Chat Session is being created

+
+

The specialist is being executed and the chat session is being created.

+

Session ID: {{ session_id }}

+

This page will automatically refresh every 2 seconds...

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index d306e79..fafeb2a 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -357,42 +357,47 @@
{% endmacro %} -{% macro render_pagination(pagination, endpoint) %} -#} +{#{% endmacro %}#} + +{% macro render_pagination(pagination, endpoint) %} + {# Deze macro is een wrapper rond de globale get_pagination_html functie #} + {# Ondersteunt nu expliciet een session_id parameter #} + {{ get_pagination_html(pagination, endpoint) }} {% endmacro %} + {% macro render_filter_field(field_name, label, options, current_value) %}
diff --git a/eveai_app/templates/scripts.html b/eveai_app/templates/scripts.html index 0e788db..c192d0e 100644 --- a/eveai_app/templates/scripts.html +++ b/eveai_app/templates/scripts.html @@ -17,6 +17,188 @@ + + +