From 5465dae52f202bac989ce781bc153829eb95d652 Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 3 Oct 2025 08:58:44 +0200 Subject: [PATCH 01/14] - Optimisation and streamlining of messages in ExecutionProgressTracker (ept) - Adaptation of ProgressTracker to handle these optimised messages - Hardening SSE-streaming in eveai_chat_client --- common/utils/execution_progress.py | 38 ++++++++++++++++--- config/static-manifest/manifest.json | 4 +- docker/nginx/Dockerfile | 2 +- .../assets/vue-components/ProgressTracker.vue | 6 +-- eveai_chat_client/views/chat_views.py | 3 +- 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/common/utils/execution_progress.py b/common/utils/execution_progress.py index fdd1616..92eda38 100644 --- a/common/utils/execution_progress.py +++ b/common/utils/execution_progress.py @@ -10,6 +10,13 @@ import time class ExecutionProgressTracker: """Tracks progress of specialist executions using Redis""" + # Normalized processing types and aliases + PT_COMPLETE = 'EVEAI_COMPLETE' + PT_ERROR = 'EVEAI_ERROR' + + _COMPLETE_ALIASES = {'EveAI Specialist Complete', 'Task Complete', 'task complete'} + _ERROR_ALIASES = {'EveAI Specialist Error', 'Task Error', 'task error'} + def __init__(self): try: # Use shared pubsub pool (lazy connect; no eager ping) @@ -40,6 +47,16 @@ class ExecutionProgressTracker: # Exhausted retries raise last_exc + def _normalize_processing_type(self, processing_type: str) -> str: + if not processing_type: + return processing_type + p = str(processing_type).strip() + if p in self._COMPLETE_ALIASES: + return self.PT_COMPLETE + if p in self._ERROR_ALIASES: + return self.PT_ERROR + return p + def send_update(self, ctask_id: str, processing_type: str, data: dict): """Send an update about execution progress""" try: @@ -47,7 +64,7 @@ class ExecutionProgressTracker: f"{data}") key = self._get_key(ctask_id) - + processing_type = self._normalize_processing_type(processing_type) update = { 'processing_type': processing_type, 'data': data, @@ -96,14 +113,16 @@ class ExecutionProgressTracker: self._retry(lambda: pubsub.subscribe(key)) try: + # Hint client reconnect interval (optional but helpful) + yield "retry: 3000\n\n" + # First yield any existing updates length = self._retry(lambda: self.redis.llen(key)) if length > 0: updates = self._retry(lambda: self.redis.lrange(key, 0, -1)) for update in updates: update_data = json.loads(update.decode('utf-8')) - # Use processing_type for the event - yield f"event: {update_data['processing_type']}\n" + update_data['processing_type'] = self._normalize_processing_type(update_data.get('processing_type')) yield f"data: {json.dumps(update_data)}\n\n" # Then listen for new updates @@ -121,13 +140,20 @@ class ExecutionProgressTracker: if message['type'] == 'message': # This is Redis pub/sub type update_data = json.loads(message['data'].decode('utf-8')) - yield f"data: {message['data'].decode('utf-8')}\n\n" + update_data['processing_type'] = self._normalize_processing_type(update_data.get('processing_type')) + yield f"data: {json.dumps(update_data)}\n\n" - # Check processing_type for completion - if update_data['processing_type'] in ['Task Complete', 'Task Error', 'EveAI Specialist Complete']: + # Unified completion check + if update_data['processing_type'] in [self.PT_COMPLETE, self.PT_ERROR]: + # Give proxies/clients a chance to flush + yield ": closing\n\n" break finally: try: pubsub.unsubscribe() except Exception: pass + try: + pubsub.close() + except Exception: + pass diff --git a/config/static-manifest/manifest.json b/config/static-manifest/manifest.json index 5df93f6..9043959 100644 --- a/config/static-manifest/manifest.json +++ b/config/static-manifest/manifest.json @@ -1,6 +1,6 @@ { - "dist/chat-client.js": "dist/chat-client.13481d75.js", - "dist/chat-client.css": "dist/chat-client.7d8832b6.css", + "dist/chat-client.js": "dist/chat-client.824c5b9d.js", + "dist/chat-client.css": "dist/chat-client.b7de7a18.css", "dist/main.js": "dist/main.f3dde0f6.js", "dist/main.css": "dist/main.c40e57ad.css" } \ No newline at end of file diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile index bacef45..cc291e2 100644 --- a/docker/nginx/Dockerfile +++ b/docker/nginx/Dockerfile @@ -16,7 +16,7 @@ RUN mkdir -p /etc/nginx/static /etc/nginx/public COPY ../../nginx/static /etc/nginx/static # Copy public files -COPY ../../nginx/public /etc/nginx/public +# COPY ../../nginx/public /etc/nginx/public # Copy site-specific configurations RUN mkdir -p /etc/nginx/sites-enabled diff --git a/eveai_chat_client/static/assets/vue-components/ProgressTracker.vue b/eveai_chat_client/static/assets/vue-components/ProgressTracker.vue index 8ccefc1..875e948 100644 --- a/eveai_chat_client/static/assets/vue-components/ProgressTracker.vue +++ b/eveai_chat_client/static/assets/vue-components/ProgressTracker.vue @@ -240,15 +240,15 @@ export default { const data = JSON.parse(event.data); console.log('Progress update:', data); - // Check voor processing_type om te bepalen welke handler te gebruiken - if (data.processing_type === 'EveAI Specialist Complete') { + // Check voor processing_type om te bepalen welke handler te gebruiken (ondersteun legacy en genormaliseerde waarden) + if (data.processing_type === 'EveAI Specialist Complete' || data.processing_type === 'EVEAI_COMPLETE') { console.log('Detected specialist complete via processing_type'); this.handleSpecialistComplete(event); return; } // Check voor andere completion statuses en errors - if (data.processing_type === 'EveAI Specialist Error') + if (data.processing_type === 'EveAI Specialist Error' || data.processing_type === 'EVEAI_ERROR') { console.log('Detected specialist error via processing_type or status'); this.handleSpecialistError(event); diff --git a/eveai_chat_client/views/chat_views.py b/eveai_chat_client/views/chat_views.py index 1a8e440..314e76f 100644 --- a/eveai_chat_client/views/chat_views.py +++ b/eveai_chat_client/views/chat_views.py @@ -301,7 +301,8 @@ def task_progress_stream(task_id): mimetype='text/event-stream', headers={ 'Cache-Control': 'no-cache', - 'X-Accel-Buffering': 'no' + 'X-Accel-Buffering': 'no', + 'Connection': 'keep-alive' } ) except Exception as e: From aab766fe5e851eaf187709fa0fd6083194495f7d Mon Sep 17 00:00:00 2001 From: Josako Date: Mon, 20 Oct 2025 15:37:36 +0200 Subject: [PATCH 02/14] - New version of RAG_SPECIALIST and RAG_AGENT, including definition of conversation_purpose and response_depth. --- config/agents/globals/RAG_AGENT/1.1.0.yaml | 4 +- config/agents/globals/RAG_AGENT/1.2.0.yaml | 28 +++ .../globals/RAG_SPECIALIST/1.2.0.yaml | 77 ++++++ config/tasks/globals/RAG_TASK/1.1.0.yaml | 19 +- .../channel_adaptation_v1_0.py | 30 +++ .../conversation_purpose_v1_0.py | 30 +++ .../response_depth/response_depth_v1_0.py | 25 ++ .../specialists/globals/RAG_SPECIALIST/1_2.py | 225 ++++++++++++++++++ 8 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 config/agents/globals/RAG_AGENT/1.2.0.yaml create mode 100644 config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml create mode 100644 eveai_chat_workers/definitions/channel_adaptation/channel_adaptation_v1_0.py create mode 100644 eveai_chat_workers/definitions/conversation_purpose/conversation_purpose_v1_0.py create mode 100644 eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py create mode 100644 eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py diff --git a/config/agents/globals/RAG_AGENT/1.1.0.yaml b/config/agents/globals/RAG_AGENT/1.1.0.yaml index d6cb7c1..8719d96 100644 --- a/config/agents/globals/RAG_AGENT/1.1.0.yaml +++ b/config/agents/globals/RAG_AGENT/1.1.0.yaml @@ -1,4 +1,4 @@ -version: "1.0.0" +version: "1.1.0" name: "Rag Agent" role: > {tenant_name} Spokesperson. {custom_role} @@ -7,7 +7,7 @@ goal: > of the current conversation. {custom_goal} backstory: > - You are the primary contact for {tenant_name}. You are known by {name}, and can be addressed by this name, or you. You are + You are the primary contact for {tenant_name}. You are known by {name}, and can be addressed by this name, or 'you'. You are a very good communicator, and adapt to the style used by the human asking for information (e.g. formal or informal). You always stay correct and polite, whatever happens. And you ensure no discriminating language is used. You are perfectly multilingual in all known languages, and do your best to answer questions in {language}, whatever diff --git a/config/agents/globals/RAG_AGENT/1.2.0.yaml b/config/agents/globals/RAG_AGENT/1.2.0.yaml new file mode 100644 index 0000000..da3a801 --- /dev/null +++ b/config/agents/globals/RAG_AGENT/1.2.0.yaml @@ -0,0 +1,28 @@ +version: "1.1.0" +name: "Rag Agent" +role: > + {tenant_name}'s Spokesperson. {custom_role} +goal: > + You get questions by a human correspondent, and give answers based on a given context, taking into account the history + of the current conversation. + {custom_goal} +backstory: > + You are the primary contact for {tenant_name}. You are known by {name}, and can be addressed by this name, or 'you'. + You are a very good communicator. + We want you to answer using the following Tone of Voice: {tone_of_voice} - {tone_of_voice_context}. + And we want you to answer using the following Language Level: {language_level} - {language_level_context}. + We want your answers to be {response_depth} - {response_depth_context}. + We want your answers to be {conversation_purpose} - {conversation_purpose_context}. + You can change Tone of Voice and Language level if required by the person you are talking to.. + You always stay correct and polite, whatever happens. And you ensure no discriminating language is used. + You are perfectly multilingual in all known languages, and do your best to answer questions in {language}, whatever + language the context provided to you is in. You are participating in a conversation, not writing e.g. an email. Do not + include a salutation or closing greeting in your answer. + {custom_backstory} +full_model_name: "mistral.mistral-medium-latest" +temperature: 0.4 +metadata: + author: "Josako" + date_added: "2025-01-08" + description: "An Agent that does RAG based on a user's question, RAG content & history" + changes: "Initial version" diff --git a/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml b/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml new file mode 100644 index 0000000..7638025 --- /dev/null +++ b/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml @@ -0,0 +1,77 @@ +version: "1.2.0" +name: "RAG Specialist" +framework: "crewai" +chat: true +configuration: + name: + name: "name" + type: "str" + description: "The name the specialist is called upon." + required: true + tone_of_voice: + name: "Tone of Voice" + description: "The tone of voice the specialist uses to communicate" + type: "enum" + allowed_values: [ "Professional & Neutral", "Warm & Empathetic", "Energetic & Enthusiastic", "Accessible & Informal", "Expert & Trustworthy", "No-nonsense & Goal-driven" ] + default: "Professional & Neutral" + required: true + language_level: + name: "Language Level" + description: "Language level to be used when communicating, relating to CEFR levels" + type: "enum" + allowed_values: [ "Basic", "Standard", "Professional" ] + default: "Standard" + required: true + response_depth: + name: "Response Depth" + description: "Response depth to be used when communicating" + type: "enum" + allowed_values: [ "Concise", "Balanced", "Detailed",] + default: "Balanced" + required: true + conversation_purpose: + name: "Conversation Purpose" + description: "Purpose of the conversation, resulting in communication style" + type: "enum" + allowed_values: [ "Informative", "Persuasive", "Supportive", "Collaborative" ] + default: "Informative" + required: true + welcome_message: + name: "Welcome Message" + type: "string" + description: "Welcome Message to be given to the end user" + required: false +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 +agents: + - type: "RAG_AGENT" + version: "1.2" +tasks: + - type: "RAG_TASK" + version: "1.1" +metadata: + author: "Josako" + date_added: "2025-01-08" + changes: "Initial version" + description: "A Specialist that performs Q&A activities" \ No newline at end of file diff --git a/config/tasks/globals/RAG_TASK/1.1.0.yaml b/config/tasks/globals/RAG_TASK/1.1.0.yaml index 457a51e..6a7351c 100644 --- a/config/tasks/globals/RAG_TASK/1.1.0.yaml +++ b/config/tasks/globals/RAG_TASK/1.1.0.yaml @@ -3,24 +3,29 @@ name: "RAG Task" task_description: > Answer the following question (in between triple £): - £££{question}£££ + £££ + {question} + £££ Base your answer on the following context (in between triple $): - $$${context}$$$ + $$$ + {context} + $$$ Take into account the following history of the conversation (in between triple €): - €€€{history}€€€ + €€€ + {history} + €€€ The HUMAN parts indicate the interactions by the end user, the AI parts are your interactions. Best Practices are: - - Answer the provided question as precisely and directly as you can, combining elements of the provided context. - - Always focus your answer on the actual HUMAN question. - - Try not to repeat your answers (preceded by AI), unless absolutely necessary. - - Focus your answer on the question at hand. + - Answer the provided question, combining elements of the provided context. + - Always focus your answer on the actual question. + - Try not to repeat your historic answers, unless absolutely necessary. - Always be friendly and helpful for the end user. {custom_description} diff --git a/eveai_chat_workers/definitions/channel_adaptation/channel_adaptation_v1_0.py b/eveai_chat_workers/definitions/channel_adaptation/channel_adaptation_v1_0.py new file mode 100644 index 0000000..0a4b17e --- /dev/null +++ b/eveai_chat_workers/definitions/channel_adaptation/channel_adaptation_v1_0.py @@ -0,0 +1,30 @@ +CHANNEL_ADAPTATION = [ + { + "name": "Mobile Chat/Text", + "description": "Short, scannable messages. Uses bullet points, emojis, and concise language.", + "when_to_use": "Instant messaging, SMS, or chatbots." + }, + { + "name": "Voice/Spoken", + "description": "Natural, conversational language. Includes pauses, intonation, and simple sentences.", + "when_to_use": "Voice assistants, phone calls, or voice-enabled apps." + }, + { + "name": "Email/Formal Text", + "description": "Structured, polished, and professional. Uses paragraphs and clear formatting.", + "when_to_use": "Emails, reports, or official documentation." + }, + { + "name": "Multimodal", + "description": "Combines text with visuals, buttons, or interactive elements for clarity and engagement.", + "when_to_use": "Websites, apps, or platforms supporting rich media." + } +] + +def get_channel_adaptation_context(channel_adaptation:str) -> str: + selected_channel_adaptation = next( + (item for item in CHANNEL_ADAPTATION if item["name"] == channel_adaptation), + None + ) + channel_adaptation_context = f"{selected_channel_adaptation['description']}" + return channel_adaptation_context diff --git a/eveai_chat_workers/definitions/conversation_purpose/conversation_purpose_v1_0.py b/eveai_chat_workers/definitions/conversation_purpose/conversation_purpose_v1_0.py new file mode 100644 index 0000000..b74cd93 --- /dev/null +++ b/eveai_chat_workers/definitions/conversation_purpose/conversation_purpose_v1_0.py @@ -0,0 +1,30 @@ +CONVERSATION_PURPOSE = [ + { + "name": "Informative", + "description": "Focus on sharing facts, explanations, or instructions.", + "when_to_use": "User seeks knowledge, clarification, or guidance." + }, + { + "name": "Persuasive", + "description": "Aim to convince, motivate, or drive action. Highlights benefits and calls to action.", + "when_to_use": "Sales, marketing, or when encouraging the user to take a specific step." + }, + { + "name": "Supportive", + "description": "Empathetic, solution-oriented, and reassuring. Prioritizes the user's needs and emotions.", + "when_to_use": "Customer support, healthcare, or emotionally sensitive situations." + }, + { + "name": "Collaborative", + "description": "Encourages dialogue, brainstorming, and co-creation. Invites user input and ideas.", + "when_to_use": "Teamwork, creative processes, or open-ended discussions." + } +] + +def get_conversation_purpose_context(conversation_purpose:str) -> str: + selected_conversation_purpose = next( + (item for item in CONVERSATION_PURPOSE if item["name"] == conversation_purpose), + None + ) + conversation_purpose_context = f"{selected_conversation_purpose['description']}" + return conversation_purpose_context diff --git a/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py b/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py new file mode 100644 index 0000000..086c2fa --- /dev/null +++ b/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py @@ -0,0 +1,25 @@ +RESPONSE_DEPTH = [ + { + "name": "Concise", + "description": "Short, direct answers. No extra explanation or context.", + "when_to_use": "Quick queries, urgent requests, or when the user prefers brevity." + }, + { + "name": "Balanced", + "description": "Clear answers with minimal context or next steps. Not too short, not too detailed.", + "when_to_use": "General interactions, FAQs, or when the user needs a mix of speed and clarity." + }, + { + "name": "Detailed", + "description": "Comprehensive answers with background, examples, or step-by-step guidance.", + "when_to_use": "Complex topics, tutorials, or when the user requests in-depth information." + } +] + +def get_response_depth_context(response_depth:str) -> str: + selected_response_depth = next( + (item for item in RESPONSE_DEPTH if item["name"] == response_depth), + None + ) + response_depth_context = f"{selected_response_depth['description']}" + return response_depth_context diff --git a/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py b/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py new file mode 100644 index 0000000..634e160 --- /dev/null +++ b/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py @@ -0,0 +1,225 @@ +import json +from os import wait +from typing import Optional, List, Dict, Any + +from crewai.flow.flow import start, listen, and_ +from flask import current_app +from pydantic import BaseModel, Field + +from common.services.utils.translation_services import TranslationServices +from common.utils.business_event_context import current_event +from eveai_chat_workers.definitions.conversation_purpose.conversation_purpose_v1_0 import \ + get_conversation_purpose_context +from eveai_chat_workers.definitions.language_level.language_level_v1_0 import get_language_level_context +from eveai_chat_workers.definitions.response_depth.response_depth_v1_0 import get_response_depth_context +from eveai_chat_workers.definitions.tone_of_voice.tone_of_voice_v1_0 import get_tone_of_voice_context +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.globals.rag.rag_v1_0 import RAGOutput +from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState + +INSUFFICIENT_INFORMATION_MESSAGES = [ + "I'm afraid I don't have enough information to answer that properly. Feel free to ask something else!", + "There isn’t enough data available right now to give you a clear answer. You're welcome to rephrase or ask a different question.", + "Sorry, I can't provide a complete answer based on the current information. Would you like to try asking something else?", + "I don’t have enough details to give you a confident answer. You can always ask another question if you’d like.", + "Unfortunately, I can’t answer that accurately with the information at hand. Please feel free to ask something else.", + "That’s a great question, but I currently lack the necessary information to respond properly. Want to ask something different?", + "I wish I could help more, but the data I have isn't sufficient to answer this. You’re welcome to explore other questions.", + "There’s not enough context for me to provide a good answer. Don’t hesitate to ask another question if you'd like!", + "I'm not able to give a definitive answer to that. Perhaps try a different question or angle?", + "Thanks for your question. At the moment, I can’t give a solid answer — but I'm here if you want to ask something else!" +] + +class SpecialistExecutor(CrewAIBaseSpecialistExecutor): + """ + type: RAG_SPECIALIST + type_version: 1.0 + RAG Specialist Executor class + """ + + def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs): + self.rag_crew = None + + super().__init__(tenant_id, specialist_id, session_id, task_id) + + @property + def type(self) -> str: + return "RAG_SPECIALIST" + + @property + def type_version(self) -> str: + return "1.2" + + def _config_task_agents(self): + self._add_task_agent("rag_task", "rag_agent") + + def _config_pydantic_outputs(self): + self._add_pydantic_output("rag_task", RAGOutput, "rag_output") + + def _config_state_result_relations(self): + self._add_state_result_relation("rag_output") + self._add_state_result_relation("citations") + + def _instantiate_specialist(self): + verbose = self.tuning + + rag_agents = [self.rag_agent] + rag_tasks = [self.rag_task] + self.rag_crew = EveAICrewAICrew( + self, + "Rag Crew", + agents=rag_agents, + tasks=rag_tasks, + verbose=verbose, + ) + + self.flow = RAGFlow( + self, + self.rag_crew, + ) + + def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("RAG Specialist execution started", {}) + + if not self._cached_session.interactions: + specialist_phase = "initial" + else: + specialist_phase = self._cached_session.interactions[-1].specialist_results.get('phase', 'initial') + + results = None + + match specialist_phase: + case "initial": + results = self.execute_initial_state(arguments, formatted_context, citations) + case "rag": + results = self.execute_rag_state(arguments, formatted_context, citations) + + self.log_tuning(f"RAG Specialist execution ended", {"Results": results.model_dump()}) + + return results + + def execute_initial_state(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("RAG Specialist initial_state execution started", {}) + + welcome_message = self.specialist.configuration.get('welcome_message', 'Welcome! You can start asking questions') + welcome_message = TranslationServices.translate(self.tenant_id, welcome_message, arguments.language) + + self.flow.state.answer = welcome_message + self.flow.state.phase = "rag" + + results = RAGSpecialistResult.create_for_type(self.type, self.type_version) + + return results + + def execute_rag_state(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("RAG Specialist rag_state execution started", {}) + + insufficient_info_message = TranslationServices.translate(self.tenant_id, + INSUFFICIENT_INFORMATION_MESSAGE, + arguments.language) + + formatted_context, citations = self._retrieve_context(arguments) + self.flow.state.citations = citations + tone_of_voice = self.specialist.configuration.get('tone_of_voice', 'Professional & Neutral') + tone_of_voice_context = get_tone_of_voice_context(tone_of_voice) + language_level = self.specialist.configuration.get('language_level', 'Standard') + language_level_context = get_language_level_context(language_level) + response_depth = self.specialist.configuration.get('response_depth', 'Balanced') + response_depth_context = get_response_depth_context(response_depth) + conversation_purpose = self.specialist.configuration.get('conversation_purpose', 'Informative') + conversation_purpose_context = get_conversation_purpose_context(conversation_purpose) + + if formatted_context: + flow_inputs = { + "language": arguments.language, + "question": arguments.question, + "context": formatted_context, + "history": self.formatted_history, + "name": self.specialist.configuration.get('name', ''), + "welcome_message": self.specialist.configuration.get('welcome_message', ''), + "tone_of_voice": tone_of_voice, + "tone_of_voice_context": tone_of_voice_context, + "language_level": language_level, + "language_level_context": language_level_context, + "response_depth": response_depth, + "response_depth_context": response_depth_context, + "conversation_purpose": conversation_purpose, + "conversation_purpose_context": conversation_purpose_context, + } + + flow_results = self.flow.kickoff(inputs=flow_inputs) + + if flow_results.rag_output.insufficient_info: + flow_results.rag_output.answer = insufficient_info_message + + rag_output = flow_results.rag_output + else: + rag_output = RAGOutput(answer=insufficient_info_message, insufficient_info=True) + + self.flow.state.rag_output = rag_output + self.flow.state.citations = citations + self.flow.state.answer = rag_output.answer + self.flow.state.phase = "rag" + + results = RAGSpecialistResult.create_for_type(self.type, self.type_version) + + return results + + +class RAGSpecialistInput(BaseModel): + language: Optional[str] = Field(None, alias="language") + question: Optional[str] = Field(None, alias="question") + context: Optional[str] = Field(None, alias="context") + history: Optional[str] = Field(None, alias="history") + name: Optional[str] = Field(None, alias="name") + welcome_message: Optional[str] = Field(None, alias="welcome_message") + + +class RAGSpecialistResult(SpecialistResult): + rag_output: Optional[RAGOutput] = Field(None, alias="Rag Output") + + +class RAGFlowState(EveAIFlowState): + """Flow state for RAG specialist that automatically updates from task outputs""" + input: Optional[RAGSpecialistInput] = None + rag_output: Optional[RAGOutput] = None + citations: Optional[List[Dict[str, Any]]] = None + + +class RAGFlow(EveAICrewAIFlow[RAGFlowState]): + def __init__(self, + specialist_executor: CrewAIBaseSpecialistExecutor, + rag_crew: EveAICrewAICrew, + **kwargs): + super().__init__(specialist_executor, "RAG Specialist Flow", **kwargs) + self.specialist_executor = specialist_executor + self.rag_crew = rag_crew + self.exception_raised = False + + @start() + def process_inputs(self): + return "" + + @listen(process_inputs) + async def execute_rag(self): + inputs = self.state.input.model_dump() + try: + crew_output = await self.rag_crew.kickoff_async(inputs=inputs) + self.specialist_executor.log_tuning("RAG Crew Output", crew_output.model_dump()) + output_pydantic = crew_output.pydantic + if not output_pydantic: + raw_json = json.loads(crew_output.raw) + output_pydantic = RAGOutput.model_validate(raw_json) + self.state.rag_output = output_pydantic + return crew_output + except Exception as e: + current_app.logger.error(f"CREW rag_crew Kickoff Error: {str(e)}") + self.exception_raised = True + raise e + + async def kickoff_async(self, inputs=None): + self.state.input = RAGSpecialistInput.model_validate(inputs) + result = await super().kickoff_async(inputs) + return self.state From 1d79a19981fa86690512cb0f097ff437e49d4291 Mon Sep 17 00:00:00 2001 From: Josako Date: Tue, 21 Oct 2025 11:00:26 +0200 Subject: [PATCH 03/14] - Refinenement of improved RAG_SPECIALIST - Changed label of RetrieverType in Retriever form --- common/utils/security_utils.py | 7 ------- config/agents/globals/RAG_AGENT/1.2.0.yaml | 2 +- .../globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml | 2 +- eveai_app/__init__.py | 1 - eveai_app/views/document_forms.py | 2 +- .../specialists/globals/RAG_SPECIALIST/1_2.py | 11 ++++++++++- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/common/utils/security_utils.py b/common/utils/security_utils.py index 68c1918..567112d 100644 --- a/common/utils/security_utils.py +++ b/common/utils/security_utils.py @@ -140,21 +140,17 @@ def enforce_tenant_consent_ui(): """Check if the user has consented to the terms of service""" path = getattr(request, 'path', '') or '' if path.startswith('/healthz') or path.startswith('/_healthz'): - current_app.logger.debug(f'Health check request, bypassing consent guard: {path}') return None if not current_user.is_authenticated: - current_app.logger.debug('Not authenticated, bypassing consent guard') return None endpoint = request.endpoint or '' if is_exempt_endpoint(endpoint) or request.method == 'OPTIONS': - current_app.logger.debug(f'Endpoint exempt from consent guard: {endpoint}') return None # Global bypass: Super User and Partner Admin always allowed if current_user.has_roles('Super User') or current_user.has_roles('Partner Admin'): - current_app.logger.debug('Global bypass: Super User or Partner Admin') return None tenant_id = getattr(current_user, 'tenant_id', None) @@ -176,16 +172,13 @@ def enforce_tenant_consent_ui(): status = ConsentStatus.NOT_CONSENTED if status == ConsentStatus.CONSENTED: - current_app.logger.debug('User has consented') return None if status == ConsentStatus.NOT_CONSENTED: - current_app.logger.debug('User has not consented') if current_user.has_roles('Tenant Admin'): return redirect(prefixed_url_for('user_bp.tenant_consent', for_redirect=True)) return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True)) if status == ConsentStatus.RENEWAL_REQUIRED: - current_app.logger.debug('Consent renewal required') if current_user.has_roles('Tenant Admin'): flash( "You need to renew your consent to our DPA or T&Cs. Failing to do so in time will stop you from accessing our services.", diff --git a/config/agents/globals/RAG_AGENT/1.2.0.yaml b/config/agents/globals/RAG_AGENT/1.2.0.yaml index da3a801..86d2d6a 100644 --- a/config/agents/globals/RAG_AGENT/1.2.0.yaml +++ b/config/agents/globals/RAG_AGENT/1.2.0.yaml @@ -1,4 +1,4 @@ -version: "1.1.0" +version: "1.2.0" name: "Rag Agent" role: > {tenant_name}'s Spokesperson. {custom_role} diff --git a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml index b894cb3..d1c9409 100644 --- a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml +++ b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml @@ -11,7 +11,7 @@ fields: email: name: "Email" type: "str" - description: "Your Name" + description: "Your Email" required: true phone: name: "Phone Number" diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index 5bcacec..5f1bd73 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -113,7 +113,6 @@ def create_app(config_file=None): # Register global consent guard via extension @app.before_request def enforce_tenant_consent(): - app.logger.debug("Enforcing tenant consent") return enforce_tenant_consent_ui() # @app.before_request diff --git a/eveai_app/views/document_forms.py b/eveai_app/views/document_forms.py index 9c11cab..8d7de25 100644 --- a/eveai_app/views/document_forms.py +++ b/eveai_app/views/document_forms.py @@ -155,7 +155,7 @@ class EditRetrieverForm(DynamicFormBase): description = TextAreaField('Description', validators=[Optional()]) # Select Field for Retriever Type (Uses the RETRIEVER_TYPES defined in config) - type = StringField('Processor Type', validators=[DataRequired()], render_kw={'readonly': True}) + type = StringField('Retriever Type', validators=[DataRequired()], render_kw={'readonly': True}) type_version = StringField('Retriever Type Version', validators=[DataRequired()], render_kw={'readonly': True}) tuning = BooleanField('Enable Tuning', default=False) diff --git a/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py b/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py index 634e160..3392708 100644 --- a/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py +++ b/eveai_chat_workers/specialists/globals/RAG_SPECIALIST/1_2.py @@ -1,4 +1,5 @@ import json +import random from os import wait from typing import Optional, List, Dict, Any @@ -117,7 +118,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor): self.log_tuning("RAG Specialist rag_state execution started", {}) insufficient_info_message = TranslationServices.translate(self.tenant_id, - INSUFFICIENT_INFORMATION_MESSAGE, + random.choice(INSUFFICIENT_INFORMATION_MESSAGES), arguments.language) formatted_context, citations = self._retrieve_context(arguments) @@ -175,6 +176,14 @@ class RAGSpecialistInput(BaseModel): history: Optional[str] = Field(None, alias="history") name: Optional[str] = Field(None, alias="name") welcome_message: Optional[str] = Field(None, alias="welcome_message") + tone_of_voice: Optional[str] = Field(None, alias="tone_of_voice") + tone_of_voice_context: Optional[str] = Field(None, alias="tone_of_voice_context") + language_level: Optional[str] = Field(None, alias="language_level") + language_level_context: Optional[str] = Field(None, alias="language_level_context") + response_depth: Optional[str] = Field(None, alias="response_depth") + response_depth_context: Optional[str] = Field(None, alias="response_depth_context") + conversation_purpose: Optional[str] = Field(None, alias="conversation_purpose") + conversation_purpose_context: Optional[str] = Field(None, alias="conversation_purpose_context") class RAGSpecialistResult(SpecialistResult): From 4ec10999251d7cf9ca932db0fd2219fc3e011cab Mon Sep 17 00:00:00 2001 From: Josako Date: Wed, 22 Oct 2025 09:57:09 +0200 Subject: [PATCH 04/14] - Changes to PROFESSIONAL_CONTACT_FORM - Introducing first user action SHARE_PROFESSIONAL_CONTACT_FORM --- .../globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml | 10 ---------- .../globals/SHARE_PROFESSIONAL_CONTACT_DATA/1.0.0.yaml | 9 +++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 config/user_actions/globals/SHARE_PROFESSIONAL_CONTACT_DATA/1.0.0.yaml diff --git a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml index d1c9409..68cd20b 100644 --- a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml +++ b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml @@ -28,16 +28,6 @@ fields: type: "str" description: "Job Title" required: false - address: - name: "Address" - type: "str" - description: "Your Address" - required: false - zip: - name: "Postal Code" - type: "str" - description: "Postal Code" - required: false city: name: "City" type: "str" diff --git a/config/user_actions/globals/SHARE_PROFESSIONAL_CONTACT_DATA/1.0.0.yaml b/config/user_actions/globals/SHARE_PROFESSIONAL_CONTACT_DATA/1.0.0.yaml new file mode 100644 index 0000000..f3049b3 --- /dev/null +++ b/config/user_actions/globals/SHARE_PROFESSIONAL_CONTACT_DATA/1.0.0.yaml @@ -0,0 +1,9 @@ +type: "SHARE_PROFESSIONAL_CONTACT_DATA" +version: "1.0.0" +name: "Share Professional Contact Data" +icon: "account_circle" +title: "Share Contact Data" +action_type: "specialist_form" +configuration: + specialist_form_name: "PROFESSIONAL_CONTACT_FORM" + specialist_form_version: "1.0.0" From 1720ddfa11b1ad7d1b92022695f5449825828d42 Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 23 Oct 2025 09:10:52 +0200 Subject: [PATCH 05/14] - cleanup of old TASKs, AGENTs and SPECIALISTs - Add additional configuration options to agent (temperature and model choice) - Define new PROOFREADING Agents and Tasks --- common/models/interaction.py | 2 + .../globals/EMAIL_CONTENT_AGENT/1.0.0.yaml | 17 - .../globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml | 16 - .../globals/IDENTIFICATION_AGENT/1.0.0.yaml | 20 -- config/agents/globals/RAG_AGENT/1.2.0.yaml | 19 +- .../RAG_COMMUNICATION_AGENT/1.0.0.yaml | 26 -- .../globals/RAG_PROOFREADER_AGENT/1.0.0.yaml | 24 ++ .../globals/SPIN_DETECTION_AGENT/1.0.0.yaml | 22 -- .../SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml | 25 -- .../globals/RAG_SPECIALIST/1.2.0.yaml | 4 + .../globals/SPIN_SPECIALIST/1.0.0.yaml | 183 ----------- .../SPIN_SPECIALIST/1.0.0_overview.svg | 299 ------------------ .../EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml | 35 -- .../EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml | 28 -- .../IDENTIFICATION_DETECTION_TASK/1.0.0.yaml | 24 -- .../IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml | 19 -- .../globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml | 27 -- .../globals/RAG_PROOFREADING_TASK/1.0.0.yaml | 26 ++ config/tasks/globals/RAG_TASK/1.1.0.yaml | 34 +- .../tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml | 18 -- .../globals/SPIN_QUESTIONS_TASK/1.0.0.yaml | 20 -- config/type_defs/agent_types.py | 26 +- config/type_defs/specialist_types.py | 4 - config/type_defs/task_types.py | 32 +- eveai_app/views/interaction_forms.py | 14 +- .../response_depth/response_depth_v1_0.py | 6 +- .../outputs/globals/rag/rag_v1_0.py | 2 +- 27 files changed, 112 insertions(+), 860 deletions(-) delete mode 100644 config/agents/globals/EMAIL_CONTENT_AGENT/1.0.0.yaml delete mode 100644 config/agents/globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml delete mode 100644 config/agents/globals/IDENTIFICATION_AGENT/1.0.0.yaml delete mode 100644 config/agents/globals/RAG_COMMUNICATION_AGENT/1.0.0.yaml create mode 100644 config/agents/globals/RAG_PROOFREADER_AGENT/1.0.0.yaml delete mode 100644 config/agents/globals/SPIN_DETECTION_AGENT/1.0.0.yaml delete mode 100644 config/agents/globals/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml delete mode 100644 config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml delete mode 100644 config/specialists/globals/SPIN_SPECIALIST/1.0.0_overview.svg delete mode 100644 config/tasks/globals/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml delete mode 100644 config/tasks/globals/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml delete mode 100644 config/tasks/globals/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml delete mode 100644 config/tasks/globals/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml delete mode 100644 config/tasks/globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml create mode 100644 config/tasks/globals/RAG_PROOFREADING_TASK/1.0.0.yaml delete mode 100644 config/tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml delete mode 100644 config/tasks/globals/SPIN_QUESTIONS_TASK/1.0.0.yaml diff --git a/common/models/interaction.py b/common/models/interaction.py index d429ce9..8a9ef6b 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -122,6 +122,8 @@ class EveAIAgent(db.Model): role = db.Column(db.Text, nullable=True) goal = db.Column(db.Text, nullable=True) backstory = db.Column(db.Text, nullable=True) + temperature = db.Column(db.Float, nullable=True) + llm_model = db.Column(db.String(50), nullable=True) tuning = db.Column(db.Boolean, nullable=True, default=False) configuration = db.Column(JSONB, nullable=True) arguments = db.Column(JSONB, nullable=True) diff --git a/config/agents/globals/EMAIL_CONTENT_AGENT/1.0.0.yaml b/config/agents/globals/EMAIL_CONTENT_AGENT/1.0.0.yaml deleted file mode 100644 index 981a88a..0000000 --- a/config/agents/globals/EMAIL_CONTENT_AGENT/1.0.0.yaml +++ /dev/null @@ -1,17 +0,0 @@ -version: "1.0.0" -name: "Email Content Agent" -role: > - Email Content Writer -goal: > - Craft a highly personalized email that resonates with the {end_user_role}'s context and identification (personal and - company if available). - {custom_goal} -backstory: > - You are an expert in writing compelling, personalized emails that capture the {end_user_role}'s attention and drive - engagement. You are perfectly multilingual, and can write the mail in the native language of the {end_user_role}. - {custom_backstory} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "An Agent that writes engaging emails." - changes: "Initial version" diff --git a/config/agents/globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml b/config/agents/globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml deleted file mode 100644 index 6581c92..0000000 --- a/config/agents/globals/EMAIL_ENGAGEMENT_AGENT/1.0.0.yaml +++ /dev/null @@ -1,16 +0,0 @@ -version: "1.0.0" -name: "Email Engagement Agent" -role: > - Engagement Optimization Specialist {custom_role} -goal: > - You ensure that the email includes strong CTAs and strategically placed engagement hooks that encourage the - {end_user_role} to take immediate action. {custom_goal} -backstory: > - You specialize in optimizing content to ensure that it not only resonates with the recipient but also encourages them - to take the desired action. - {custom_backstory} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "An Agent that ensures the email is engaging and lead to maximal desired action" - changes: "Initial version" diff --git a/config/agents/globals/IDENTIFICATION_AGENT/1.0.0.yaml b/config/agents/globals/IDENTIFICATION_AGENT/1.0.0.yaml deleted file mode 100644 index 09e08e5..0000000 --- a/config/agents/globals/IDENTIFICATION_AGENT/1.0.0.yaml +++ /dev/null @@ -1,20 +0,0 @@ -version: "1.0.0" -name: "Identification Agent" -role: > - Identification Administrative force. {custom_role} -goal: > - You are an administrative force that tries to gather identification information to complete the administration of an - end-user, the company he or she works for, through monitoring conversations and advising on questions to help you do - your job. You are responsible for completing the company's backend systems (like CRM, ERP, ...) with inputs from the - end user in the conversation. - {custom_goal} -backstory: > - You are and administrative force for {company}, and very proficient in gathering information for the company's backend - systems. You do so by monitoring conversations between one of your colleagues (e.g. sales, finance, support, ...) and - an end user. You ask your colleagues to request additional information to complete your task. - {custom_backstory} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "An Agent that gathers administrative information" - changes: "Initial version" diff --git a/config/agents/globals/RAG_AGENT/1.2.0.yaml b/config/agents/globals/RAG_AGENT/1.2.0.yaml index 86d2d6a..360c6e9 100644 --- a/config/agents/globals/RAG_AGENT/1.2.0.yaml +++ b/config/agents/globals/RAG_AGENT/1.2.0.yaml @@ -7,19 +7,20 @@ goal: > of the current conversation. {custom_goal} backstory: > - You are the primary contact for {tenant_name}. You are known by {name}, and can be addressed by this name, or 'you'. - You are a very good communicator. - We want you to answer using the following Tone of Voice: {tone_of_voice} - {tone_of_voice_context}. - And we want you to answer using the following Language Level: {language_level} - {language_level_context}. - We want your answers to be {response_depth} - {response_depth_context}. - We want your answers to be {conversation_purpose} - {conversation_purpose_context}. - You can change Tone of Voice and Language level if required by the person you are talking to.. + You are the primary contact for {tenant_name}, and have been it's spokesperson for a very long time. You are used to + addressing customers, prospects, press, ... + You are known by {name}, and can be addressed by this name, or 'you'. + You are a very good communicator, that knows how to adapt his style to the public your interacting with. You always stay correct and polite, whatever happens. And you ensure no discriminating language is used. You are perfectly multilingual in all known languages, and do your best to answer questions in {language}, whatever - language the context provided to you is in. You are participating in a conversation, not writing e.g. an email. Do not - include a salutation or closing greeting in your answer. + language the context provided to you is in. You are participating in a conversation, not writing e.g. an email or + essay. Do not include a salutation or closing greeting in your answer. {custom_backstory} full_model_name: "mistral.mistral-medium-latest" +allowed_models: + - "mistral.mistral-small-latest" + - "mistral.mistral-medium-latest" + - "mistral.magistral-medium-latest" temperature: 0.4 metadata: author: "Josako" diff --git a/config/agents/globals/RAG_COMMUNICATION_AGENT/1.0.0.yaml b/config/agents/globals/RAG_COMMUNICATION_AGENT/1.0.0.yaml deleted file mode 100644 index a0c8c82..0000000 --- a/config/agents/globals/RAG_COMMUNICATION_AGENT/1.0.0.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: "1.0.0" -name: "Rag Communication Agent" -role: > - {company} Interaction Responsible. {custom_role} -goal: > - Your team has collected answers to a question asked. But it also created some additional questions to be asked. You - ensure the necessary answers are returned, and make an informed selection of the additional questions that can be - asked (combining them when appropriate), ensuring the human you're communicating to does not get overwhelmed. - {custom_goal} -backstory: > - You are the online communication expert for {company}. You handled a lot of online communications with both customers - and internal employees. You are a master in redacting one coherent reply in a conversation that includes all the - answers, and a selection of additional questions to be asked in a conversation. Although your backoffice team might - want to ask a myriad of questions, you understand that doesn't fit with the way humans communicate. You know how to - combine multiple related questions, and understand how to interweave the questions in the answers when related. - You are perfectly multilingual in all known languages, and do your best to answer questions in {language}, whatever - language the context provided to you is in. Also, ensure that questions asked do not contradict with the answers - given, or aren't obsolete given the answer provided. - You are participating in a conversation, not writing e.g. an email. Do not include a salutation or closing greeting - in your answer. - {custom_backstory} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "An Agent that consolidates both answers and questions in a consistent reply" - changes: "Initial version" diff --git a/config/agents/globals/RAG_PROOFREADER_AGENT/1.0.0.yaml b/config/agents/globals/RAG_PROOFREADER_AGENT/1.0.0.yaml new file mode 100644 index 0000000..fb95a44 --- /dev/null +++ b/config/agents/globals/RAG_PROOFREADER_AGENT/1.0.0.yaml @@ -0,0 +1,24 @@ +version: "1.0.0" +name: "Rag Proofreader Agent" +role: > + Proofreader for {tenant_name}. {custom_role} +goal: > + You get a prepared answer to be send out, and adapt it to comply to best practices. + {custom_goal} +backstory: > + You are the primary contact for {tenant_name}, and have been it's spokesperson for a very long time. You are used to + addressing customers, prospects, press, ... + You are known by {name}, and can be addressed by this name, or 'you'. + You review communications and ensure they are clear and follow best practices. + {custom_backstory} +full_model_name: "mistral.mistral-medium-latest" +allowed_models: + - "mistral.mistral-small-latest" + - "mistral.mistral-medium-latest" + - "mistral.magistral-medium-latest" +temperature: 0.4 +metadata: + author: "Josako" + date_added: "2025-10-22" + description: "An Agent that does QA Activities on provided answers" + changes: "Initial version" diff --git a/config/agents/globals/SPIN_DETECTION_AGENT/1.0.0.yaml b/config/agents/globals/SPIN_DETECTION_AGENT/1.0.0.yaml deleted file mode 100644 index 2348272..0000000 --- a/config/agents/globals/SPIN_DETECTION_AGENT/1.0.0.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: "1.0.0" -name: "SPIN Sales Assistant" -role: > - Sales Assistant for {company} on {products}. {custom_role} -goal: > - Your main job is to help your sales specialist to analyze an ongoing conversation with a customer, and detect - SPIN-related information. {custom_goal} -backstory: > - You are a sales assistant for {company} on {products}. You are known by {name}, and can be addressed by this name, or you. You are - trained to understand an analyse ongoing conversations. Your are proficient in detecting SPIN-related information in a - conversation. - SPIN stands for: - - Situation information - Understanding the customer's current context - - Problem information - Uncovering challenges and pain points - - Implication information - Exploring consequences of those problems - - Need-payoff information - Helping customers realize value of solutions - {custom_backstory} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "An Agent that detects SPIN information in an ongoing conversation" - changes: "Initial version" diff --git a/config/agents/globals/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml b/config/agents/globals/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml deleted file mode 100644 index 9fb68c3..0000000 --- a/config/agents/globals/SPIN_SALES_SPECIALIST_AGENT/1.0.0.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: "1.0.0" -name: "SPIN Sales Specialist" -role: > - Sales Specialist for {company} on {products}. {custom_role} -goal: > - Your main job is to do sales using the SPIN selling methodology in a first conversation with a potential customer. - {custom_goal} -backstory: > - You are a sales specialist for {company} on {products}. You are known by {name}, and can be addressed by this name, - or you. You have an assistant that provides you with already detected SPIN-information in an ongoing conversation. You - decide on follow-up questions for more in-depth information to ensure we get the required information that may lead to - selling {products}. - SPIN stands for: - - Situation information - Understanding the customer's current context - - Problem information - Uncovering challenges and pain points - - Implication information - Exploring consequences of those problems - - Need-payoff information - Helping customers realize value of solutions - {custom_backstory} - You are acquainted with the following product information: - {product_information} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "An Agent that asks for Follow-up questions for SPIN-process" - changes: "Initial version" diff --git a/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml b/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml index 7638025..4e94b9d 100644 --- a/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml +++ b/config/specialists/globals/RAG_SPECIALIST/1.2.0.yaml @@ -67,9 +67,13 @@ results: agents: - type: "RAG_AGENT" version: "1.2" + - type: "RAG_PROOFREADER_AGENT" + version: "1.0" tasks: - type: "RAG_TASK" version: "1.1" + - type: "RAG_PROOFREADING_TASK" + version: "1.0" metadata: author: "Josako" date_added: "2025-01-08" diff --git a/config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml b/config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml deleted file mode 100644 index 8fcc113..0000000 --- a/config/specialists/globals/SPIN_SPECIALIST/1.0.0.yaml +++ /dev/null @@ -1,183 +0,0 @@ -version: "1.0.0" -name: "Spin Sales Specialist" -framework: "crewai" -chat: true -configuration: - name: - name: "name" - type: "str" - description: "The name the specialist is called upon." - required: true - company: - name: "company" - type: "str" - description: "The name of your company. If not provided, your tenant's name will be used." - required: false - products: - name: "products" - type: "List[str]" - description: "The products or services you're providing" - required: false - product_information: - name: "product_information" - type: "text" - description: "Information on the products you are selling, such as ICP (Ideal Customer Profile), Pitch, ..." - required: false - engagement_options: - name: "engagement_options" - type: "text" - description: "Engagement options such as email, phone number, booking link, ..." - tenant_language: - name: "tenant_language" - type: "str" - description: "The language code used for internal information. If not provided, the tenant's default language will be used" - required: false - nr_of_questions: - name: "nr_of_questions" - type: "int" - description: "The maximum number of questions to formulate extra questions" - required: true - default: 3 -arguments: - language: - name: "Language" - type: "str" - description: "Language code to be used for receiving questions and giving answers" - required: true - query: - name: "query" - type: "str" - description: "Query or response to process" - required: true - identification: - name: "identification" - type: "text" - description: "Initial identification information when available" - required: false -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/globals/SPIN_SPECIALIST/1.0.0_overview.svg b/config/specialists/globals/SPIN_SPECIALIST/1.0.0_overview.svg deleted file mode 100644 index c374ee4..0000000 --- a/config/specialists/globals/SPIN_SPECIALIST/1.0.0_overview.svg +++ /dev/null @@ -1,299 +0,0 @@ - - - - - - - - - - - - - - - - EveAI Agent - - - - - - - - - - - - - - - - - - - - Identification Agent - - - - - - - - - - - - - - - - SPIN Sales Assistant - - - - - - - - - - - - - - - - SPIN Sales Specialist - - - - - - - - - - - - - - - - - - - - Identification Agent - - - - - - - - - - - - - - - - Consolidation Agent - - - - - - - - - - - - - - - - RAG Agent - - - - - - - - - - - - - - - - - EveAI Task - - - - - - - - - - - - - RAG Task - - - - - - - - - - - - - - - SPIN Detection - - - - - - - - - - - - - - - - - - - Identification Gathering - - - - - - - - - - - - - - - - - - - Identification Questions - - - - - - - - - - - - - - - Consolidate Q&A - - - - - - - - - - - - - - - SPIN Questions - - - - - - - - - - - - - EveAI Tool - - - - - - - - - - - - - - - - RAG Task - - - - - - - - - - - - - Retrieval - - - - - - - Q&A Processing - - - - - - - - - - - - - - - - - diff --git a/config/tasks/globals/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml b/config/tasks/globals/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml deleted file mode 100644 index d958958..0000000 --- a/config/tasks/globals/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml +++ /dev/null @@ -1,35 +0,0 @@ -version: "1.0.0" -name: "Email Lead Draft Creation" -task_description: > - Craft a highly personalized email using the lead's name, job title, company information, and any relevant personal or - company achievements when available. The email should speak directly to the lead's interests and the needs - of their company. - This mail is the consequence of a first conversation. You have information available from that conversation in the - - SPIN-context (in between triple %) - - personal and company information (in between triple $) - Information might be missing however, as it might not be gathered in that first conversation. - Don't use any salutations or closing remarks, nor too complex sentences. - - Our Company and Product: - - Company Name: {company} - - Products: {products} - - Product information: {product_information} - - {customer_role}'s Identification: - $$${Identification}$$$ - - SPIN context: - %%%{SPIN}%%% - - {custom_description} -expected_output: > - A personalized email draft that: - - Addresses the lead by name - - Acknowledges their role and company - - Highlights how {company} can meet their specific needs or interests - {customer_expected_output} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "Email Drafting Task towards a Lead" - changes: "Initial version" diff --git a/config/tasks/globals/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml b/config/tasks/globals/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml deleted file mode 100644 index 1302c57..0000000 --- a/config/tasks/globals/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: "1.0.0" -name: "Email Lead Engagement Creation" -task_description: > - Review a personalized email and optimize it with strong CTAs and engagement hooks. Keep in mind that this email is - the consequence of a first conversation. - Don't use any salutations or closing remarks, nor too complex sentences. Keep it short and to the point. - Don't use any salutations or closing remarks, nor too complex sentences. - Ensure the email encourages the lead to schedule a meeting or take - another desired action immediately. - - Our Company and Product: - - Company Name: {company} - - Products: {products} - - Product information: {product_information} - - Engagement options: - {engagement_options} - - {custom_description} -expected_output: > - An optimized email ready for sending, complete with: - - Strong CTAs - - Strategically placed engagement hooks that encourage immediate action -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "Make an Email draft more engaging" - changes: "Initial version" diff --git a/config/tasks/globals/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml b/config/tasks/globals/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml deleted file mode 100644 index d8ca17d..0000000 --- a/config/tasks/globals/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml +++ /dev/null @@ -1,24 +0,0 @@ -version: "1.0.0" -name: "Identification Gathering" -task_description: > - You are asked to gather lead information in a conversation with a new prospect. This is information about the person - participating in the conversation, and information on the company he or she is working for. Try to be as precise as - possible. - Take into account information already gathered in the historic lead info (between triple backquotes) and add - information found in the latest reply. Also, some identification information may be given by the end user. - - historic lead info: - ```{historic_lead_info}``` - latest reply: - {query} - identification: - {identification} - - {custom_description} -expected_output: > - -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "A Task that gathers identification information from a conversation" - changes: "Initial version" diff --git a/config/tasks/globals/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/globals/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml deleted file mode 100644 index 624c033..0000000 --- a/config/tasks/globals/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: "1.0.0" -name: "Define Identification Questions" -task_description: > - Gather the identification information gathered by your team mates. Ensure no information in the historic lead - information (in between triple backquotes) and the latest reply of the user is lost. - Define questions to be asked to complete the personal and company information for the end user in the conversation. - historic lead info: - ```{historic_lead_info}``` - latest reply: - {query} - - {custom_description} -expected_output: > - -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "A Task to define identification (person & company) questions" - changes: "Initial version" diff --git a/config/tasks/globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml b/config/tasks/globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml deleted file mode 100644 index a70f1e1..0000000 --- a/config/tasks/globals/RAG_CONSOLIDATION_TASK/1.0.0.yaml +++ /dev/null @@ -1,27 +0,0 @@ -version: "1.0.0" -name: "Rag Consolidation" -task_description: > - Your teams have collected answers to a user's query (in between triple backquotes), and collected additional follow-up - questions (in between triple %) to reach their goals. Ensure the answers are provided, and select a maximum of - {nr_of_questions} out of the additional questions to be asked in order not to overwhelm the user. The questions are - in no specific order, so don't just pick the first ones. Make a good mixture of different types of questions, - different topics or subjects! - Questions are to be asked when your team proposes questions. You ensure both answers and additional questions are - bundled into 1 clear communication back to the user. Use {language} for your consolidated communication. - Be sure to format your answer in markdown when appropriate. Ensure enumerations or bulleted lists are formatted as - lists in markdown. - {custom_description} - - Anwers: - ```{prepared_answers}``` - - Additional Questions: - %%%{additional_questions}%%% - -expected_output: > - {custom_expected_output} -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "A Task to consolidate questions and answers" - changes: "Initial version" diff --git a/config/tasks/globals/RAG_PROOFREADING_TASK/1.0.0.yaml b/config/tasks/globals/RAG_PROOFREADING_TASK/1.0.0.yaml new file mode 100644 index 0000000..17322bc --- /dev/null +++ b/config/tasks/globals/RAG_PROOFREADING_TASK/1.0.0.yaml @@ -0,0 +1,26 @@ +version: "1.0.0" +name: "RAG QA Task" +task_description: > + You have to improve this first draft answering the following question: + + £££ + {question} + £££ + + We want you to pay extra attention and adapt to the following requirements: + + - The answer uses the following Tone of Voice: {tone_of_voice}, i.e. {tone_of_voice_context} + - The answer is adapted to the following Language Level: {language_level}, i.e. {language_level_context} + - The answer is suited to be {conversation_purpose}, i.e. {conversation_purpose_context} + - And we want the answer to have the following depth: {response_depth}, i.e. {response_depth_context} + + Ensure the following {language} is used. + If there was insufficient information to answer, answer "I have insufficient information to answer this + question." and give the appropriate indication. +expected_output: > + Your answer. +metadata: + author: "Josako" + date_added: "2025-01-08" + description: "A Task that gives RAG-based answers" + changes: "Initial version" diff --git a/config/tasks/globals/RAG_TASK/1.1.0.yaml b/config/tasks/globals/RAG_TASK/1.1.0.yaml index 6a7351c..8887007 100644 --- a/config/tasks/globals/RAG_TASK/1.1.0.yaml +++ b/config/tasks/globals/RAG_TASK/1.1.0.yaml @@ -7,19 +7,9 @@ task_description: > {question} £££ - Base your answer on the following context (in between triple $): - - $$$ - {context} - $$$ - - Take into account the following history of the conversation (in between triple €): - - €€€ - {history} - €€€ - - The HUMAN parts indicate the interactions by the end user, the AI parts are your interactions. + Base your answer on the context below, in between triple '$'. + Take into account the history of the conversion , in between triple '€'. The parts in the history preceded by 'HUMAN' + indicate the interactions by the end user, the parts preceded with 'AI' are your interactions. Best Practices are: @@ -27,11 +17,29 @@ task_description: > - Always focus your answer on the actual question. - Try not to repeat your historic answers, unless absolutely necessary. - Always be friendly and helpful for the end user. + + Tune your answer with the following: + + - You use the following Tone of Voice for your answer: {tone_of_voice}, i.e. {tone_of_voice_context} + - You use the following Language Level for your answer: {language_level}, i.e. {language_level_context} + - The purpose of the conversation is to be {conversation_purpose}, i.e. {conversation_purpose_context} + - We expect you to answer with the following depth: {response_depth}, i.e. {response_depth_context} {custom_description} + Use the following {language} in your communication. If the question cannot be answered using the given context, answer "I have insufficient information to answer this question." and give the appropriate indication. + + Context: + $$$ + {context} + $$$ + + History: + €€€ + {history} + €€€ expected_output: > Your answer. metadata: diff --git a/config/tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml b/config/tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml deleted file mode 100644 index f496764..0000000 --- a/config/tasks/globals/SPIN_DETECT_TASK/1.0.0.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "1.0.0" -name: "SPIN Information Detection" -task_description: > - Complement the historic SPIN context (in between triple backquotes) with information found in the latest reply of the - end user. - {custom_description} - Use the following {tenant_language} to define the SPIN-elements. - Historic SPIN: - ```{historic_spin}``` - Latest reply: - {query} -expected_output: > - -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "A Task that performs SPIN Information Detection" - changes: "Initial version" diff --git a/config/tasks/globals/SPIN_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/globals/SPIN_QUESTIONS_TASK/1.0.0.yaml deleted file mode 100644 index 87f722f..0000000 --- a/config/tasks/globals/SPIN_QUESTIONS_TASK/1.0.0.yaml +++ /dev/null @@ -1,20 +0,0 @@ -version: "1.0.0" -name: "SPIN Question Identification" -task_description: > - Revise the final SPIN provided by your colleague, and ensure no information is lost from the histoic SPIN and the - latest reply from the user. Define the top questions that need to be asked to understand the full SPIN context - of the customer. If you think this user could be a potential customer, please indicate so. - {custom_description} - Use the following {tenant_language} to define the SPIN-elements. If you have a satisfying SPIN context, just skip and - don't ask for more information or confirmation. - Historic SPIN: - ```{historic_spin}``` - Latest reply: - {query} -expected_output: > - -metadata: - author: "Josako" - date_added: "2025-01-08" - description: "A Task that identifies questions to complete the SPIN context in a conversation" - changes: "Initial version" diff --git a/config/type_defs/agent_types.py b/config/type_defs/agent_types.py index a64b1c5..d30d67d 100644 --- a/config/type_defs/agent_types.py +++ b/config/type_defs/agent_types.py @@ -1,32 +1,12 @@ # Agent Types AGENT_TYPES = { - "EMAIL_CONTENT_AGENT": { - "name": "Email Content Agent", - "description": "An Agent that writes engaging emails.", - }, - "EMAIL_ENGAGEMENT_AGENT": { - "name": "Email Engagement Agent", - "description": "An Agent that ensures the email is engaging and lead to maximal desired action", - }, - "IDENTIFICATION_AGENT": { - "name": "Identification Agent", - "description": "An Agent that gathers identification information", - }, "RAG_AGENT": { "name": "Rag Agent", "description": "An Agent that does RAG based on a user's question, RAG content & history", }, - "RAG_COMMUNICATION_AGENT": { - "name": "Rag Communication Agent", - "description": "An Agent that consolidates both answers and questions in a consistent reply", - }, - "SPIN_DETECTION_AGENT": { - "name": "SPIN Sales Assistant", - "description": "An Agent that detects SPIN information in an ongoing conversation", - }, - "SPIN_SALES_SPECIALIST_AGENT": { - "name": "SPIN Sales Specialist", - "description": "An Agent that asks for Follow-up questions for SPIN-process", + "RAG_PROOFREADER_AGENT": { + "name": "Rag Proofreader Agent", + "description": "An Agent that checks the quality of RAG answers and adapts when required", }, "TRAICIE_HR_BP_AGENT": { "name": "Traicie HR BP Agent", diff --git a/config/type_defs/specialist_types.py b/config/type_defs/specialist_types.py index a7274f0..6d5f9be 100644 --- a/config/type_defs/specialist_types.py +++ b/config/type_defs/specialist_types.py @@ -9,10 +9,6 @@ SPECIALIST_TYPES = { "description": "Q&A through Partner RAG Specialist (for documentation purposes)", "partner": "evie_partner" }, - "SPIN_SPECIALIST": { - "name": "Spin Sales Specialist", - "description": "A specialist that allows to answer user queries, try to get SPIN-information and Identification", - }, "TRAICIE_ROLE_DEFINITION_SPECIALIST": { "name": "Traicie Role Definition Specialist", "description": "Assistant Defining Competencies and KO Criteria", diff --git a/config/type_defs/task_types.py b/config/type_defs/task_types.py index 3c7df5a..a602d5c 100644 --- a/config/type_defs/task_types.py +++ b/config/type_defs/task_types.py @@ -1,36 +1,16 @@ # Agent Types TASK_TYPES = { - "EMAIL_LEAD_DRAFTING_TASK": { - "name": "Email Lead Draft Creation", - "description": "Email Drafting Task towards a Lead", - }, - "EMAIL_LEAD_ENGAGEMENT_TASK": { - "name": "Email Lead Engagement Creation", - "description": "Make an Email draft more engaging", - }, - "IDENTIFICATION_DETECTION_TASK": { - "name": "Identification Gathering", - "description": "A Task that gathers identification information from a conversation", - }, - "IDENTIFICATION_QUESTIONS_TASK": { - "name": "Define Identification Questions", - "description": "A Task to define identification (person & company) questions", - }, "RAG_TASK": { "name": "RAG Task", "description": "A Task that gives RAG-based answers", }, - "SPIN_DETECT_TASK": { - "name": "SPIN Information Detection", - "description": "A Task that performs SPIN Information Detection", + "ADVANCED_RAG_TASK": { + "name": "Advanced RAG Task", + "description": "A Task that gives RAG-based answers taking into account previous questions, tone of voice and language level", }, - "SPIN_QUESTIONS_TASK": { - "name": "SPIN Question Identification", - "description": "A Task that identifies questions to complete the SPIN context in a conversation", - }, - "RAG_CONSOLIDATION_TASK": { - "name": "RAG Consolidation", - "description": "A Task to consolidate questions and answers", + "RAG_PROOFREADING_TASK": { + "name": "Rag Proofreading Task", + "description": "A Task that performs RAG Proofreading", }, "TRAICIE_GET_COMPETENCIES_TASK": { "name": "Traicie Get Competencies", diff --git a/eveai_app/views/interaction_forms.py b/eveai_app/views/interaction_forms.py index 783f2df..d31be67 100644 --- a/eveai_app/views/interaction_forms.py +++ b/eveai_app/views/interaction_forms.py @@ -2,8 +2,8 @@ from flask import session from flask_wtf import FlaskForm from wtforms import (StringField, BooleanField, SelectField, TextAreaField) from wtforms.fields.datetime import DateField -from wtforms.fields.numeric import IntegerField -from wtforms.validators import DataRequired, Length, Optional +from wtforms.fields.numeric import IntegerField, FloatField +from wtforms.validators import DataRequired, Length, Optional, NumberRange from wtforms_sqlalchemy.fields import QuerySelectMultipleField @@ -91,6 +91,16 @@ class EditEveAIAgentForm(BaseEditComponentForm): role = TextAreaField('Role', validators=[Optional()]) goal = TextAreaField('Goal', validators=[Optional()]) backstory = TextAreaField('Backstory', validators=[Optional()]) + temperature = FloatField('Temperature', validators=[Optional(), NumberRange(min=0, max=1)]) + llm_model = SelectField('LLM Model', validators=[Optional()]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + agent_config = cache_manager.agents_config_cache.get_config(self.type, self.type_version) + if agent_config.get('allowed_models', None): + self.llm_model.choices = agent_config.allowed_models + else: + self.llm_model.choices = agent_config.get('full_model_name', 'mistral.mistral-medium-latest') class EditEveAITaskForm(BaseEditComponentForm): diff --git a/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py b/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py index 086c2fa..4b7974c 100644 --- a/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py +++ b/eveai_chat_workers/definitions/response_depth/response_depth_v1_0.py @@ -1,17 +1,17 @@ RESPONSE_DEPTH = [ { "name": "Concise", - "description": "Short, direct answers. No extra explanation or context.", + "description": "Short, direct answers. No extra explanation or context. Remove unnecessary details. Only a few sentences.", "when_to_use": "Quick queries, urgent requests, or when the user prefers brevity." }, { "name": "Balanced", - "description": "Clear answers with minimal context or next steps. Not too short, not too detailed.", + "description": "Clear answers with minimal context or next steps. Not too short, not too detailed. Max 2 paragraphs.", "when_to_use": "General interactions, FAQs, or when the user needs a mix of speed and clarity." }, { "name": "Detailed", - "description": "Comprehensive answers with background, examples, or step-by-step guidance.", + "description": "Comprehensive answers with background, examples, ... the answer can be detailed with chapters, paragraphs, ...", "when_to_use": "Complex topics, tutorials, or when the user requests in-depth information." } ] diff --git a/eveai_chat_workers/outputs/globals/rag/rag_v1_0.py b/eveai_chat_workers/outputs/globals/rag/rag_v1_0.py index 0f66f66..fc18464 100644 --- a/eveai_chat_workers/outputs/globals/rag/rag_v1_0.py +++ b/eveai_chat_workers/outputs/globals/rag/rag_v1_0.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field class RAGOutput(BaseModel): - answer: str = Field(None, description="Answer to the questions asked, in Markdown format.") + answer: str = Field(None, description="Final answer to the question asked, in Markdown format.") insufficient_info: bool = Field(None, description="An indication if there's insufficient information to answer") model_config = { From 2bc5832db6aa7aaf23ea623d898e34b337e746cd Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 23 Oct 2025 10:17:57 +0200 Subject: [PATCH 06/14] - Temporarily remove PartnerRagRetriever model (as it is not used yet) - Ensure errors are being logged when migrating tenants - Ensure migrations directory is copied into eveai_app --- common/models/user.py | 22 +++++++++++----------- docker/eveai_app/Dockerfile | 1 + migrations/tenant/env.py | 16 ++++++++++------ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/common/models/user.py b/common/models/user.py index 4fff20e..91b7770 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -386,14 +386,14 @@ class TranslationCache(db.Model): last_used_at = db.Column(db.DateTime, nullable=True) -class PartnerRAGRetriever(db.Model): - __bind_key__ = 'public' - __table_args__ = ( - db.PrimaryKeyConstraint('tenant_id', 'retriever_id'), - db.UniqueConstraint('partner_id', 'tenant_id', 'retriever_id'), - {'schema': 'public'}, - ) - - partner_id = db.Column(db.Integer, db.ForeignKey('public.partner.id'), nullable=False) - tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) - retriever_id = db.Column(db.Integer, nullable=False) +# class PartnerRAGRetriever(db.Model): +# __bind_key__ = 'public' +# __table_args__ = ( +# db.PrimaryKeyConstraint('tenant_id', 'retriever_id'), +# db.UniqueConstraint('partner_id', 'tenant_id', 'retriever_id'), +# {'schema': 'public'}, +# ) +# +# partner_id = db.Column(db.Integer, db.ForeignKey('public.partner.id'), nullable=False) +# tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) +# retriever_id = db.Column(db.Integer, nullable=False) diff --git a/docker/eveai_app/Dockerfile b/docker/eveai_app/Dockerfile index 7b88912..d4193d8 100644 --- a/docker/eveai_app/Dockerfile +++ b/docker/eveai_app/Dockerfile @@ -2,3 +2,4 @@ FROM registry.ask-eve-ai-local.com/josakola/eveai-base:latest # Copy the source code into the container. COPY eveai_app /app/eveai_app COPY content /app/content +COPY migrations /app/migrations diff --git a/migrations/tenant/env.py b/migrations/tenant/env.py index 9cb8044..e1bd58c 100644 --- a/migrations/tenant/env.py +++ b/migrations/tenant/env.py @@ -70,10 +70,12 @@ target_db = current_app.extensions['migrate'].db def get_public_table_names(): # TODO: This function should include the necessary functionality to automatically retrieve table names - return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage', - 'business_event_log', 'tenant_project', 'partner', 'partner_service', 'invoice', 'license_period', - 'license_change_log', 'partner_service_license_tier', 'payment', 'partner_tenant', 'tenant_make', - 'specialist_magic_link_tenant', 'translation_cache'] + return ['tenant', 'role', 'roles_users', 'user', 'tenant_domain', 'tenant_project', 'tenant_make', 'partner', + 'partner_service', 'partner_tenant', 'tenant_consent', 'consent_version', 'specialist_magic_link_tenant', + 'translation_cache', + 'business_event_log', 'license', 'license_tier', 'partner_service_license_tier', 'license_period', + 'license_usage', 'payment', 'invoice', 'license_change_log', + ] PUBLIC_TABLES = get_public_table_names() logger.info(f"Public tables: {PUBLIC_TABLES}") @@ -147,7 +149,7 @@ def run_migrations_online(): for tenant in tenants: try: os.environ['TENANT_ID'] = str(tenant) - logger.info(f"Migrating tenant: {tenant}") + logger.info(f"🚧 Migrating tenant: {tenant}") # set search path on the connection, which ensures that # PostgreSQL will emit all CREATE / ALTER / DROP statements # in terms of this schema by default @@ -169,11 +171,13 @@ def run_migrations_online(): with context.begin_transaction(): context.run_migrations() + logger.info(f"✅ Migration successfully completed for tenant: {tenant}") + # for checking migrate or upgrade is running if getattr(config.cmd_opts, "autogenerate", False): break except Exception as e: - continue + logger.error(f"🚨 An error occurred during migration: \n{e}") if context.is_offline_mode(): From 3aa2158a17d653685e9f857821d417515894ddd4 Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 23 Oct 2025 10:18:31 +0200 Subject: [PATCH 07/14] - Updated manifest.json --- config/static-manifest/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/static-manifest/manifest.json b/config/static-manifest/manifest.json index f5e2a58..de931d8 100644 --- a/config/static-manifest/manifest.json +++ b/config/static-manifest/manifest.json @@ -1,6 +1,6 @@ { - "dist/chat-client.js": "dist/chat-client.59b28883.js", - "dist/chat-client.css": "dist/chat-client.79757200.css", + "dist/chat-client.js": "dist/chat-client.8fea5d6b.js", + "dist/chat-client.css": "dist/chat-client.22ac21c3.css", "dist/main.js": "dist/main.c5b0c81d.js", "dist/main.css": "dist/main.06893f70.css" } \ No newline at end of file From a43825f5f08f967a39f0ebba87ae9ca552dc7fce Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 24 Oct 2025 10:17:20 +0200 Subject: [PATCH 08/14] - Ensure correct editing of additional Agent configuration possiblities when editing a specialist. --- eveai_app/views/interaction_forms.py | 52 +++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/eveai_app/views/interaction_forms.py b/eveai_app/views/interaction_forms.py index d31be67..7899917 100644 --- a/eveai_app/views/interaction_forms.py +++ b/eveai_app/views/interaction_forms.py @@ -1,4 +1,4 @@ -from flask import session +from flask import session, current_app from flask_wtf import FlaskForm from wtforms import (StringField, BooleanField, SelectField, TextAreaField) from wtforms.fields.datetime import DateField @@ -95,12 +95,54 @@ class EditEveAIAgentForm(BaseEditComponentForm): llm_model = SelectField('LLM Model', validators=[Optional()]) def __init__(self, *args, **kwargs): + obj = kwargs.get('obj') + agent_type = None + agent_type_version = None + if obj: + agent_type = obj.type + agent_type_version = obj.type_version + current_llm_model = obj.llm_model + super().__init__(*args, **kwargs) - agent_config = cache_manager.agents_config_cache.get_config(self.type, self.type_version) - if agent_config.get('allowed_models', None): - self.llm_model.choices = agent_config.allowed_models + + # Choices instellen + if agent_type and agent_type_version: + current_app.logger.info(f"Loading agent config for {agent_type} {agent_type_version}") + self._agent_config = cache_manager.agents_config_cache.get_config(agent_type, agent_type_version) + allowed_models = self._agent_config.get('allowed_models', None) + full_model_name = self._agent_config.get('full_model_name', 'mistral.mistral-medium-latest') + if allowed_models: + # Converteer lijst van strings naar lijst van tuples (value, label) + self.llm_model.choices = [(model, model) for model in allowed_models] + # Als er een waarde in de database staat, voeg die dan toe als die niet in de lijst zou voorkomen + if current_llm_model and current_llm_model not in allowed_models: + current_app.logger.warning( + f"Current model {current_llm_model} not in allowed models, adding it to choices" + ) + self.llm_model.choices.append((current_llm_model, f"{current_llm_model} (legacy)")) + else: + # Gebruik full_model_name als fallback + self.llm_model.choices = [(full_model_name, full_model_name)] + + # Als er GEEN waarde in de database staat, toon dan de default uit de config + if not current_llm_model: + self.llm_model.data = full_model_name else: - self.llm_model.choices = agent_config.get('full_model_name', 'mistral.mistral-medium-latest') + self.llm_model.choices = [('mistral.mistral-medium-latest', 'mistral.mistral-medium-latest')] + + def populate_obj(self, obj): + """Override populate_obj om de None waarde te behouden indien nodig""" + original_llm_model = obj.llm_model + + # Roep de parent populate_obj aan + super().populate_obj(obj) + + # Als de originele waarde None was EN de nieuwe waarde gelijk is aan de config default, + # herstel dan de None waarde + if original_llm_model is None and self._agent_config: + full_model_name = self._agent_config.get('full_model_name', 'mistral.mistral-medium-latest') + if obj.llm_model == full_model_name: + obj.llm_model = None class EditEveAITaskForm(BaseEditComponentForm): From c523250ccbdfc461337ca07cf68483a7311fed46 Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 24 Oct 2025 10:18:24 +0200 Subject: [PATCH 09/14] - Forgotten migration? --- ...8be_add_temperature_and_allowed_models_.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 migrations/tenant/versions/0e3dd4d218be_add_temperature_and_allowed_models_.py diff --git a/migrations/tenant/versions/0e3dd4d218be_add_temperature_and_allowed_models_.py b/migrations/tenant/versions/0e3dd4d218be_add_temperature_and_allowed_models_.py new file mode 100644 index 0000000..76384a6 --- /dev/null +++ b/migrations/tenant/versions/0e3dd4d218be_add_temperature_and_allowed_models_.py @@ -0,0 +1,31 @@ +"""Add Temperature and Allowed Models configuration to EveAIAgent + +Revision ID: 0e3dd4d218be +Revises: 5e3dd539e5c1 +Create Date: 2025-10-22 14:40:15.686495 + +""" +from alembic import op +import sqlalchemy as sa +import pgvector +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '0e3dd4d218be' +down_revision = '5e3dd539e5c1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('eve_ai_agent', sa.Column('temperature', sa.Float(), nullable=True)) + op.add_column('eve_ai_agent', sa.Column('llm_model', sa.String(length=50), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('eve_ai_agent', 'llm_model') + op.drop_column('eve_ai_agent', 'temperature') + # ### end Alembic commands ### From b3ee2f7ce923d20206f8abbf6f72384d9f31c425 Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 24 Oct 2025 11:42:50 +0200 Subject: [PATCH 10/14] Bug Fix where - in exceptional cases - a connection without correct search path could be used (out of the connection pool). --- common/utils/database.py | 93 ++++++++++++++++++++++-- eveai_chat_workers/chat_session_cache.py | 9 +++ eveai_chat_workers/tasks.py | 25 +++++-- 3 files changed, 115 insertions(+), 12 deletions(-) diff --git a/common/utils/database.py b/common/utils/database.py index 1f14869..bb05d0f 100644 --- a/common/utils/database.py +++ b/common/utils/database.py @@ -1,9 +1,9 @@ """Database related functions""" from os import popen -from sqlalchemy import text +from sqlalchemy import text, event from sqlalchemy.schema import CreateSchema from sqlalchemy.exc import InternalError -from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.orm import sessionmaker, scoped_session, Session as SASession from sqlalchemy.exc import SQLAlchemyError from flask import current_app @@ -16,6 +16,66 @@ class Database: def __init__(self, tenant: str) -> None: self.schema = str(tenant) + # --- Session / Transaction events to ensure correct search_path per transaction --- + @event.listens_for(SASession, "after_begin") + def _set_search_path_per_tx(session, transaction, connection): + """Ensure each transaction sees the right tenant schema, regardless of + which pooled connection is used. Uses SET LOCAL so it is scoped to the tx. + """ + schema = session.info.get("tenant_schema") + if schema: + try: + connection.exec_driver_sql(f'SET LOCAL search_path TO "{schema}", public') + # Optional visibility/logging for debugging + sp = connection.exec_driver_sql("SHOW search_path").scalar() + try: + current_app.logger.info(f"DBCTX tx_begin conn_id={id(connection.connection)} search_path={sp}") + except Exception: + pass + except Exception as e: + try: + current_app.logger.error(f"Failed to SET LOCAL search_path for schema {schema}: {e!r}") + except Exception: + pass + + def _log_db_context(self, origin: str = "") -> None: + """Log key DB context info to diagnose schema/search_path issues. + + Collects and logs in a single structured line: + - current_database() + - inet_server_addr(), inet_server_port() + - SHOW search_path + - current_schema() + - to_regclass('interaction') + - to_regclass('.interaction') + """ + try: + db_name = db.session.execute(text("SELECT current_database()"))\ + .scalar() + host = db.session.execute(text("SELECT inet_server_addr()"))\ + .scalar() + port = db.session.execute(text("SELECT inet_server_port()"))\ + .scalar() + search_path = db.session.execute(text("SHOW search_path"))\ + .scalar() + current_schema = db.session.execute(text("SELECT current_schema()"))\ + .scalar() + reg_unqualified = db.session.execute(text("SELECT to_regclass('interaction')"))\ + .scalar() + qualified = f"{self.schema}.interaction" + reg_qualified = db.session.execute( + text("SELECT to_regclass(:qn)"), + {"qn": qualified} + ).scalar() + current_app.logger.info( + "DBCTX origin=%s db=%s host=%s port=%s search_path=%s current_schema=%s to_regclass(interaction)=%s to_regclass(%s)=%s", + origin, db_name, host, port, search_path, current_schema, reg_unqualified, qualified, reg_qualified + ) + except SQLAlchemyError as e: + current_app.logger.error( + f"DBCTX logging failed at {origin} for schema {self.schema}: {e!r}" + ) + def get_engine(self): """create new schema engine""" return db.engine.execution_options( @@ -52,9 +112,32 @@ class Database: current_app.logger.error(f"💔 Error creating tables for schema {self.schema}: {e.args}") def switch_schema(self): - """switch between tenant/public database schema""" - db.session.execute(text(f'set search_path to "{self.schema}", public')) - db.session.commit() + """switch between tenant/public database schema with diagnostics logging""" + # Record the desired tenant schema on the active Session so events can use it + try: + db.session.info["tenant_schema"] = self.schema + except Exception: + pass + # Log the context before switching + self._log_db_context("before_switch") + try: + db.session.execute(text(f'set search_path to "{self.schema}", public')) + db.session.commit() + except SQLAlchemyError as e: + # Rollback on error to avoid InFailedSqlTransaction and log details + try: + db.session.rollback() + except Exception: + pass + current_app.logger.error( + f"Error switching search_path to {self.schema}: {e!r}" + ) + # Also log context after failure + self._log_db_context("after_switch_failed") + # Re-raise to let caller decide handling if needed + raise + # Log the context after successful switch + self._log_db_context("after_switch") def migrate_tenant_schema(self): """migrate tenant database schema for new tenant""" diff --git a/eveai_chat_workers/chat_session_cache.py b/eveai_chat_workers/chat_session_cache.py index 1e2a58e..6da4c28 100644 --- a/eveai_chat_workers/chat_session_cache.py +++ b/eveai_chat_workers/chat_session_cache.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from flask import current_app from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import joinedload +from sqlalchemy import text from common.extensions import db, cache_manager from common.models.interaction import ChatSession, Interaction @@ -111,6 +112,14 @@ class ChatSessionCacheHandler(CacheHandler[CachedSession]): Note: Only adds the interaction if it has an answer """ + # Log connection context right before any potential lazy load of interaction properties + try: + sp = db.session.execute(text("SHOW search_path")).scalar() + cid = id(db.session.connection().connection) + current_app.logger.info(f"DBCTX before_lazy_load conn_id={cid} search_path={sp}") + except Exception: + pass + if not interaction.specialist_results: return # Skip incomplete interactions diff --git a/eveai_chat_workers/tasks.py b/eveai_chat_workers/tasks.py index b3411ea..596355a 100644 --- a/eveai_chat_workers/tasks.py +++ b/eveai_chat_workers/tasks.py @@ -351,16 +351,27 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict return response except Exception as e: + # Ensure DB session is usable after an error + try: + db.session.rollback() + except Exception: + pass stacktrace = traceback.format_exc() ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)}) current_app.logger.error(f'execute_specialist: Error executing specialist: {e}\n{stacktrace}') - new_interaction.processing_error = str(e)[:255] - try: - db.session.add(new_interaction) - db.session.commit() - except SQLAlchemyError as e: - stacktrace = traceback.format_exc() - current_app.logger.error(f'execute_specialist: Error updating interaction: {e}\n{stacktrace}') + if new_interaction is not None: + new_interaction.processing_error = str(e)[:255] + try: + db.session.add(new_interaction) + db.session.commit() + except SQLAlchemyError as e: + # On failure to update, rollback and log + try: + db.session.rollback() + except Exception: + pass + stacktrace = traceback.format_exc() + current_app.logger.error(f'execute_specialist: Error updating interaction: {e}\n{stacktrace}') self.update_state(state=states.FAILURE) raise From d6041ebb274cee1dd4647b87435d1f6303f2889c Mon Sep 17 00:00:00 2001 From: Josako Date: Tue, 28 Oct 2025 17:35:36 +0100 Subject: [PATCH 11/14] - Specialist Editor Change (all components in same overview), modal editors to allow for more complex configuration of Agents, Tasks and Tools - Strengthening dynamic forms --- common/utils/cache/base.py | 4 +- config/agents/globals/RAG_AGENT/1.2.0.yaml | 2 +- config/static-manifest/manifest.json | 2 +- eveai_app/static/assets/js/eveai-list-view.js | 166 +++++- eveai_app/templates/eveai_list_view.html | 104 ++-- .../templates/interaction/component.html | 60 +- .../interaction/edit_specialist.html | 530 ++++++++---------- eveai_app/templates/scripts.html | 50 +- eveai_app/views/dynamic_form_base.py | 3 +- eveai_app/views/interaction_forms.py | 35 +- eveai_app/views/interaction_views.py | 99 +++- .../list_views/interaction_list_views.py | 71 +++ nginx/frontend_src/js/tabulator-setup.js | 37 ++ 13 files changed, 736 insertions(+), 427 deletions(-) diff --git a/common/utils/cache/base.py b/common/utils/cache/base.py index 4e7cd01..46de589 100644 --- a/common/utils/cache/base.py +++ b/common/utils/cache/base.py @@ -121,7 +121,7 @@ class CacheHandler(Generic[T]): region_name = getattr(self.region, 'name', 'default_region') key = CacheKey({k: identifiers[k] for k in self._key_components}) - return f"{region_name}_{self.prefix}:{str(key)}" + return f"{region_name}:{self.prefix}:{str(key)}" def get(self, creator_func, **identifiers) -> T: """ @@ -179,7 +179,7 @@ class CacheHandler(Generic[T]): Deletes all keys that start with the region prefix. """ # Construct the pattern for all keys in this region - pattern = f"{self.region}_{self.prefix}:*" + pattern = f"{self.region}:{self.prefix}:*" # Assuming Redis backend with dogpile, use `delete_multi` or direct Redis access if hasattr(self.region.backend, 'client'): diff --git a/config/agents/globals/RAG_AGENT/1.2.0.yaml b/config/agents/globals/RAG_AGENT/1.2.0.yaml index 360c6e9..bb64f2d 100644 --- a/config/agents/globals/RAG_AGENT/1.2.0.yaml +++ b/config/agents/globals/RAG_AGENT/1.2.0.yaml @@ -21,7 +21,7 @@ allowed_models: - "mistral.mistral-small-latest" - "mistral.mistral-medium-latest" - "mistral.magistral-medium-latest" -temperature: 0.4 +temperature: 0.3 metadata: author: "Josako" date_added: "2025-01-08" diff --git a/config/static-manifest/manifest.json b/config/static-manifest/manifest.json index de931d8..70bb5e1 100644 --- a/config/static-manifest/manifest.json +++ b/config/static-manifest/manifest.json @@ -1,6 +1,6 @@ { "dist/chat-client.js": "dist/chat-client.8fea5d6b.js", "dist/chat-client.css": "dist/chat-client.22ac21c3.css", - "dist/main.js": "dist/main.c5b0c81d.js", + "dist/main.js": "dist/main.6a617099.js", "dist/main.css": "dist/main.06893f70.css" } \ No newline at end of file diff --git a/eveai_app/static/assets/js/eveai-list-view.js b/eveai_app/static/assets/js/eveai-list-view.js index 31cdc76..798a876 100644 --- a/eveai_app/static/assets/js/eveai-list-view.js +++ b/eveai_app/static/assets/js/eveai-list-view.js @@ -12,6 +12,21 @@ if (typeof window.EveAI === 'undefined') { window.EveAI.ListView = { // Opslag voor lijst-view instanties instances: {}, + // Registry voor custom formatters (kan uitgebreid worden door templates) + formatters: { + // typeBadge: toont een badge voor agent/task/tool (robuust met Bootstrap 5 classes) + typeBadge: function(cell) { + const raw = (cell.getValue() || '').toString(); + const val = raw.toLowerCase(); + const map = { + 'agent': { cls: 'badge text-bg-primary', label: 'Agent' }, + 'task': { cls: 'badge text-bg-warning', label: 'Task' }, + 'tool': { cls: 'badge text-bg-info', label: 'Tool' }, + }; + const conf = map[val] || { cls: 'badge text-bg-secondary', label: (raw ? raw : 'Item') }; + return `${conf.label}`; + } + }, /** * Initialiseer een Tabulator lijst-view @@ -24,19 +39,50 @@ window.EveAI.ListView = { const defaultConfig = { height: 600, layout: "fitColumns", - selectable: true, + selectable: 1, // single-row selection for consistent UX across Tabulator versions movableColumns: true, pagination: "local", paginationSize: 15, paginationSizeSelector: [10, 15, 20, 50, 100], }; + // Respecteer eventueel meegegeven tableHeight alias + if (config && typeof config.tableHeight !== 'undefined' && typeof config.height === 'undefined') { + config.height = config.tableHeight; + } + + // Los string-formatters op naar functies via registry + if (config && Array.isArray(config.columns)) { + config.columns = config.columns.map(col => { + const newCol = { ...col }; + if (typeof newCol.formatter === 'string' && window.EveAI && window.EveAI.ListView && window.EveAI.ListView.formatters) { + const key = newCol.formatter.trim(); + const fmt = window.EveAI.ListView.formatters[key]; + if (typeof fmt === 'function') { + newCol.formatter = fmt; + } + } + return newCol; + }); + } + const tableConfig = {...defaultConfig, ...config}; + // Enforce single-row selection across Tabulator versions + if (tableConfig.selectable === true) { + tableConfig.selectable = 1; + } + + // Respect and enforce unique row index across Tabulator versions + if (config && typeof config.index === 'string' && config.index) { + // Tabulator v4/v5 + tableConfig.index = config.index; + // Tabulator v6+ (alias) + tableConfig.indexField = config.index; + } + // Voeg rij selectie event toe tableConfig.rowSelectionChanged = (data, rows) => { - console.log("Rij selectie gewijzigd:", rows.length, "rijen geselecteerd"); - // Update de geselecteerde rij in onze instance if (this.instances[elementId]) { this.instances[elementId].selectedRow = rows.length > 0 ? rows[0].getData() : null; @@ -60,6 +106,26 @@ window.EveAI.ListView = { this.updateActionButtons(elementId); }, 0); + // Forceer enkelvoudige selectie op klik voor consistente UX + try { + table.on('rowClick', function(e, row) { + // voorkom multi-select: altijd eerst deselecteren + row.getTable().deselectRow(); + row.select(); + }); + table.on('cellClick', function(e, cell) { + const row = cell.getRow(); + row.getTable().deselectRow(); + row.select(); + }); + // Optioneel: cursor als pointer bij hover + table.on('rowFormatter', function(row) { + row.getElement().style.cursor = 'pointer'; + }); + } catch (e) { + console.warn('Kon click-selectie handlers niet registreren:', e); + } + return table; } catch (error) { console.error(`Fout bij het initialiseren van Tabulator voor ${elementId}:`, error); @@ -168,16 +234,94 @@ window.EveAI.ListView = { } }; -// Functie om beschikbaar te maken in templates -function handleListViewAction(action, requiresSelection) { - // Vind het tableId op basis van de button die is aangeklikt - const target = event?.target || event?.srcElement; +// Functie om beschikbaar te maken in templates (met guard en expliciete event-parameter) +if (typeof window.handleListViewAction !== 'function') { + window.handleListViewAction = function(action, requiresSelection, e) { + const evt = e || undefined; // geen gebruik van deprecated window.event + const target = evt && (evt.target || evt.srcElement); - // Vind het formulier en tableId op basis daarvan - const form = target ? target.closest('form') : null; - const tableId = form ? form.id.replace('-form', '') : 'unknown_table'; + // 1) Bepaal tableId zo robuust mogelijk + let tableId = null; + if (target) { + // Zoek het werkelijke trigger element (button/anchor) i.p.v. een child node + const trigger = (typeof target.closest === 'function') ? target.closest('button, a') : target; - return window.EveAI.ListView.handleAction(action, requiresSelection, tableId); + // a) Respecteer expliciete data-attribute op knop + tableId = trigger && trigger.getAttribute ? trigger.getAttribute('data-table-id') : null; + + if (!tableId) { + // b) Zoek dichtstbijzijnde container met een tabulator-list-view erin + const containerEl = trigger && typeof trigger.closest === 'function' ? trigger.closest('.container') : null; + const scopedTable = containerEl ? containerEl.querySelector('.tabulator-list-view') : null; + tableId = scopedTable ? scopedTable.id : null; + } + if (!tableId) { + // c) Val terug op dichtstbijzijnde form id-afleiding (enkel als het een -form suffix heeft) + const form = trigger && typeof trigger.closest === 'function' ? trigger.closest('form') : null; + if (form && typeof form.id === 'string' && form.id.endsWith('-form')) { + tableId = form.id.slice(0, -'-form'.length); + } + } + } + if (!tableId) { + // d) Laatste redmiddel: pak de eerste tabulator-list-view op de pagina + const anyTable = document.querySelector('.tabulator-list-view'); + tableId = anyTable ? anyTable.id : null; + } + if (!tableId) { + console.error('Kan tableId niet bepalen voor action:', action); + return false; + } + + const listView = window.EveAI && window.EveAI.ListView ? window.EveAI.ListView : null; + const instance = listView && listView.instances ? listView.instances[tableId] : null; + + // 2) Indien selectie vereist, enforce + if (requiresSelection === true) { + if (!instance || !instance.selectedRow) { + // Probeer nog de Tabulator API als instance ontbreekt + try { + const table = Tabulator.findTable(`#${tableId}`)[0]; + const rows = table ? table.getSelectedRows() : []; + if (!rows || rows.length === 0) { + alert('Selecteer eerst een item uit de lijst.'); + return false; + } + if (instance) instance.selectedRow = rows[0].getData(); + } catch (_) { + alert('Selecteer eerst een item uit de lijst.'); + return false; + } + } + } + + // 3) Embedded handler krijgt voorrang + const embeddedHandlers = listView && listView.embeddedHandlers ? listView.embeddedHandlers : null; + const embedded = embeddedHandlers && embeddedHandlers[tableId]; + if (typeof embedded === 'function') { + try { + embedded(action, instance ? instance.selectedRow : null, tableId); + return true; + } catch (err) { + console.error('Embedded handler error:', err); + return false; + } + } + + // 4) Vervallen naar legacy form submit/JS handler + if (listView && typeof listView.handleAction === 'function') { + return listView.handleAction(action, requiresSelection, tableId); + } + + // 5) Allerbeste laatste fallback – probeer form submit met hidden inputs + const actionInput = document.getElementById(`${tableId}-action`); + if (actionInput) actionInput.value = action; + const form = document.getElementById(`${tableId}-form`); + if (form) { form.submit(); return true; } + + console.error('Geen geldige handler gevonden voor action:', action); + return false; + } } console.log('EveAI List View component geladen'); diff --git a/eveai_app/templates/eveai_list_view.html b/eveai_app/templates/eveai_list_view.html index c9723fb..140b455 100644 --- a/eveai_app/templates/eveai_list_view.html +++ b/eveai_app/templates/eveai_list_view.html @@ -16,7 +16,7 @@
{% for action in actions if action.position != 'right' %} + {% endif %} + + +
+ + {% if enable_reset_defaults %} + {% endif %} -
- - -
{% endblock %} {% block content_footer %} diff --git a/eveai_app/templates/interaction/edit_specialist.html b/eveai_app/templates/interaction/edit_specialist.html index 3ea1708..4e23a57 100644 --- a/eveai_app/templates/interaction/edit_specialist.html +++ b/eveai_app/templates/interaction/edit_specialist.html @@ -32,23 +32,8 @@ - - - @@ -63,17 +48,17 @@ {% endfor %} -
-
-
-
-
- Specialist Overview -
-
-
-
-
+{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{# Specialist Overview#} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} @@ -88,79 +73,28 @@ {% endfor %} - -
+ +
- {{ render_selectable_table( - headers=["Agent ID", "Name", "Type", "Type Version"], - rows=agent_rows if agent_rows else [], - selectable=True, - id="agentsTable", - is_component_selector=True - ) }} -
- +
+ + +
+
+
+ +
+
- - -
-
-
- {{ render_selectable_table( - headers=["Task ID", "Name", "Type", "Type Version"], - rows=task_rows if task_rows else [], - selectable=True, - id="tasksTable", - is_component_selector=True - ) }} -
- -
-
-
-
- - -
-
-
- {{ render_selectable_table( - headers=["Tool ID", "Name", "Type", "Type Version"], - rows=tool_rows if tool_rows else [], - selectable=True, - id="toolsTable", - is_component_selector=True - ) }} -
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
@@ -170,245 +104,219 @@ + + + {% endblock %} {% block scripts %} {{ super() }} + diff --git a/eveai_app/templates/scripts.html b/eveai_app/templates/scripts.html index 1397bd8..f298616 100644 --- a/eveai_app/templates/scripts.html +++ b/eveai_app/templates/scripts.html @@ -122,21 +122,41 @@ function validateTableSelection(formId) { }