From a43825f5f08f967a39f0ebba87ae9ca552dc7fce Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 24 Oct 2025 10:17:20 +0200 Subject: [PATCH 1/3] - 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 b3ee2f7ce923d20206f8abbf6f72384d9f31c425 Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 24 Oct 2025 11:42:50 +0200 Subject: [PATCH 2/3] 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 3/3] - 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) { }