Merge branch 'feature/Adding_Additional_configuration_and_capabilities_to_RAG_Agent' into develop

This commit is contained in:
Josako
2025-10-28 17:35:55 +01:00
16 changed files with 890 additions and 436 deletions

View File

@@ -121,7 +121,7 @@ class CacheHandler(Generic[T]):
region_name = getattr(self.region, 'name', 'default_region') region_name = getattr(self.region, 'name', 'default_region')
key = CacheKey({k: identifiers[k] for k in self._key_components}) 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: def get(self, creator_func, **identifiers) -> T:
""" """
@@ -179,7 +179,7 @@ class CacheHandler(Generic[T]):
Deletes all keys that start with the region prefix. Deletes all keys that start with the region prefix.
""" """
# Construct the pattern for all keys in this region # 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 # Assuming Redis backend with dogpile, use `delete_multi` or direct Redis access
if hasattr(self.region.backend, 'client'): if hasattr(self.region.backend, 'client'):

View File

@@ -1,9 +1,9 @@
"""Database related functions""" """Database related functions"""
from os import popen from os import popen
from sqlalchemy import text from sqlalchemy import text, event
from sqlalchemy.schema import CreateSchema from sqlalchemy.schema import CreateSchema
from sqlalchemy.exc import InternalError 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 sqlalchemy.exc import SQLAlchemyError
from flask import current_app from flask import current_app
@@ -16,6 +16,66 @@ class Database:
def __init__(self, tenant: str) -> None: def __init__(self, tenant: str) -> None:
self.schema = str(tenant) 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('<tenant>.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): def get_engine(self):
"""create new schema engine""" """create new schema engine"""
return db.engine.execution_options( 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}") current_app.logger.error(f"💔 Error creating tables for schema {self.schema}: {e.args}")
def switch_schema(self): def switch_schema(self):
"""switch between tenant/public database schema""" """switch between tenant/public database schema with diagnostics logging"""
db.session.execute(text(f'set search_path to "{self.schema}", public')) # Record the desired tenant schema on the active Session so events can use it
db.session.commit() 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): def migrate_tenant_schema(self):
"""migrate tenant database schema for new tenant""" """migrate tenant database schema for new tenant"""

View File

@@ -21,7 +21,7 @@ allowed_models:
- "mistral.mistral-small-latest" - "mistral.mistral-small-latest"
- "mistral.mistral-medium-latest" - "mistral.mistral-medium-latest"
- "mistral.magistral-medium-latest" - "mistral.magistral-medium-latest"
temperature: 0.4 temperature: 0.3
metadata: metadata:
author: "Josako" author: "Josako"
date_added: "2025-01-08" date_added: "2025-01-08"

View File

@@ -1,6 +1,6 @@
{ {
"dist/chat-client.js": "dist/chat-client.8fea5d6b.js", "dist/chat-client.js": "dist/chat-client.8fea5d6b.js",
"dist/chat-client.css": "dist/chat-client.22ac21c3.css", "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" "dist/main.css": "dist/main.06893f70.css"
} }

View File

@@ -12,6 +12,21 @@ if (typeof window.EveAI === 'undefined') {
window.EveAI.ListView = { window.EveAI.ListView = {
// Opslag voor lijst-view instanties // Opslag voor lijst-view instanties
instances: {}, 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 `<span class="${conf.cls}">${conf.label}</span>`;
}
},
/** /**
* Initialiseer een Tabulator lijst-view * Initialiseer een Tabulator lijst-view
@@ -24,19 +39,50 @@ window.EveAI.ListView = {
const defaultConfig = { const defaultConfig = {
height: 600, height: 600,
layout: "fitColumns", layout: "fitColumns",
selectable: true, selectable: 1, // single-row selection for consistent UX across Tabulator versions
movableColumns: true, movableColumns: true,
pagination: "local", pagination: "local",
paginationSize: 15, paginationSize: 15,
paginationSizeSelector: [10, 15, 20, 50, 100], 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}; 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 // Voeg rij selectie event toe
tableConfig.rowSelectionChanged = (data, rows) => { tableConfig.rowSelectionChanged = (data, rows) => {
console.log("Rij selectie gewijzigd:", rows.length, "rijen geselecteerd");
// Update de geselecteerde rij in onze instance // Update de geselecteerde rij in onze instance
if (this.instances[elementId]) { if (this.instances[elementId]) {
this.instances[elementId].selectedRow = rows.length > 0 ? rows[0].getData() : null; this.instances[elementId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
@@ -60,6 +106,26 @@ window.EveAI.ListView = {
this.updateActionButtons(elementId); this.updateActionButtons(elementId);
}, 0); }, 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; return table;
} catch (error) { } catch (error) {
console.error(`Fout bij het initialiseren van Tabulator voor ${elementId}:`, 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 // Functie om beschikbaar te maken in templates (met guard en expliciete event-parameter)
function handleListViewAction(action, requiresSelection) { if (typeof window.handleListViewAction !== 'function') {
// Vind het tableId op basis van de button die is aangeklikt window.handleListViewAction = function(action, requiresSelection, e) {
const target = event?.target || event?.srcElement; 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 // 1) Bepaal tableId zo robuust mogelijk
const form = target ? target.closest('form') : null; let tableId = null;
const tableId = form ? form.id.replace('-form', '') : 'unknown_table'; 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'); console.log('EveAI List View component geladen');

View File

@@ -16,7 +16,7 @@
<div class="{% if right_actions %}col{% else %}col-12{% endif %}"> <div class="{% if right_actions %}col{% else %}col-12{% endif %}">
{% for action in actions if action.position != 'right' %} {% for action in actions if action.position != 'right' %}
<button type="button" <button type="button"
onclick="handleListViewAction('{{ action.value }}', {{ action.requiresSelection|tojson }})" onclick="handleListViewAction('{{ action.value }}', {{ action.requiresSelection|tojson }}, event)"
class="btn {{ action.class|default('btn-primary') }} me-2 {% if action.requiresSelection %}requires-selection{% endif %}" class="btn {{ action.class|default('btn-primary') }} me-2 {% if action.requiresSelection %}requires-selection{% endif %}"
{% if action.requiresSelection %}disabled{% endif %}> {% if action.requiresSelection %}disabled{% endif %}>
{{ action.text }} {{ action.text }}
@@ -27,7 +27,7 @@
<div class="col-auto text-end"> <div class="col-auto text-end">
{% for action in actions if action.position == 'right' %} {% for action in actions if action.position == 'right' %}
<button type="button" <button type="button"
onclick="handleListViewAction('{{ action.value }}', {{ action.requiresSelection|tojson }})" onclick="handleListViewAction('{{ action.value }}', {{ action.requiresSelection|tojson }}, event)"
class="btn {{ action.class|default('btn-primary') }} ms-2 {% if action.requiresSelection %}requires-selection{% endif %}" class="btn {{ action.class|default('btn-primary') }} ms-2 {% if action.requiresSelection %}requires-selection{% endif %}"
{% if action.requiresSelection %}disabled{% endif %}> {% if action.requiresSelection %}disabled{% endif %}>
{{ action.text }} {{ action.text }}
@@ -59,7 +59,7 @@ document.addEventListener('DOMContentLoaded', function() {
button.disabled = true; button.disabled = true;
button.classList.add('disabled'); button.classList.add('disabled');
} }
}); })
// Voeg de benodigde functies toe als ze nog niet bestaan // Voeg de benodigde functies toe als ze nog niet bestaan
if (!window.EveAI.ListView.initialize) { if (!window.EveAI.ListView.initialize) {
@@ -96,6 +96,21 @@ document.addEventListener('DOMContentLoaded', function() {
return null; return null;
} }
// Register embedded handlers registry and custom formatters if not present
window.EveAI.ListView.embeddedHandlers = window.EveAI.ListView.embeddedHandlers || {};
window.EveAI.ListView.formatters = window.EveAI.ListView.formatters || {};
// Custom formatter: typeBadge (agent/task/tool)
window.EveAI.ListView.formatters.typeBadge = function(cell, formatterParams, onRendered) {
const val = (cell.getValue() || '').toString();
const map = {
'agent': { cls: 'badge bg-purple', label: 'Agent' },
'task': { cls: 'badge bg-orange', label: 'Task' },
'tool': { cls: 'badge bg-teal', label: 'Tool' },
};
const conf = map[val] || { cls: 'badge bg-secondary', label: val };
return `<span class="${conf.cls}">${conf.label}</span>`;
};
try { try {
// Create Tabulator table // Create Tabulator table
const tableContainer = document.createElement('div'); const tableContainer = document.createElement('div');
@@ -296,11 +311,13 @@ document.addEventListener('DOMContentLoaded', function() {
// Voeg formattering toe volgens de juiste manier per versie // Voeg formattering toe volgens de juiste manier per versie
if (col.formatter) { if (col.formatter) {
if (isTabulator6Plus) { // Resolve custom formatter name from registry when provided as string
column.formatterParams = { formatter: col.formatter }; let fmt = col.formatter;
} else { if (typeof fmt === 'string' && window.EveAI && window.EveAI.ListView && window.EveAI.ListView.formatters && window.EveAI.ListView.formatters[fmt]) {
column.formatter = col.formatter; fmt = window.EveAI.ListView.formatters[fmt];
} }
// Apply formatter for both Tabulator 5 and 6
column.formatter = fmt;
} }
// Voeg filtering toe volgens de juiste manier per versie // Voeg filtering toe volgens de juiste manier per versie
@@ -423,67 +440,62 @@ document.addEventListener('DOMContentLoaded', function() {
// Definieer de handleListViewAction functie als deze nog niet bestaat // Definieer de handleListViewAction functie als deze nog niet bestaat
if (typeof window.handleListViewAction !== 'function') { if (typeof window.handleListViewAction !== 'function') {
window.handleListViewAction = function(action, requiresSelection, e) { window.handleListViewAction = function(action, requiresSelection, e) {
// Gebruik explicit de event parameter om de browser event warning te vermijden // Prefer embedded handler when available (embedded mode)
const evt = e || window.event; const evt = e || window.event;
const target = evt?.target || evt?.srcElement; const target = evt?.target || evt?.srcElement;
// Voorkom acties vanuit gedisabled buttons // Determine tableId scoped to the closest container of this partial
if (target && (target.disabled || target.classList.contains('disabled'))) { let tableId = null;
console.log('Button actie geblokkeerd: button is disabled'); if (target) {
return false; const containerEl = target.closest('.container');
const scopedTable = containerEl ? containerEl.querySelector('.tabulator-list-view') : null;
tableId = scopedTable ? scopedTable.id : null;
} }
// Vind het tableId op basis van het formulier waarin we zitten if (!tableId) {
const form = target ? target.closest('form') : null; // fallback to previous behavior
const tableId = form ? form.id.replace('-form', '') : document.querySelector('.tabulator-list-view')?.id; const form = target ? target.closest('form') : null;
tableId = form ? form.id.replace('-form', '') : document.querySelector('.tabulator-list-view')?.id;
}
if (!tableId) { if (!tableId) {
console.error('Kan tableId niet bepalen voor action:', action); console.error('Kan tableId niet bepalen voor action:', action);
return false; return false;
} }
// Controleer direct of de button disabled zou moeten zijn // Enforce selection when required
if (requiresSelection === true) { if (requiresSelection === true) {
const instance = window.EveAI.ListView.instances[tableId]; const instance = window.EveAI.ListView.instances[tableId];
if (!instance || !instance.selectedRow) { if (!instance || !instance.selectedRow) {
console.log('Button actie geblokkeerd: geen rij geselecteerd');
return false;
}
}
// Als EveAI.ListView beschikbaar is, gebruik dan de handleAction functie
if (window.EveAI && window.EveAI.ListView && typeof window.EveAI.ListView.handleAction === 'function') {
return window.EveAI.ListView.handleAction(action, requiresSelection, tableId);
}
// Fallback naar de originele implementatie
const actionInput = document.getElementById(`${tableId}-action`);
if (actionInput) {
actionInput.value = action;
}
// Controleer of er een rij geselecteerd is indien nodig
if (requiresSelection) {
const selectedRowInput = document.getElementById(`${tableId}-selected-row`);
if (!selectedRowInput || !selectedRowInput.value) {
alert('Selecteer eerst een item uit de lijst.'); alert('Selecteer eerst een item uit de lijst.');
return false; return false;
} }
} }
// Verstuur het formulier met behoud van de originele form action // Embedded handler first
if (form) { const embedded = window.EveAI.ListView.embeddedHandlers && window.EveAI.ListView.embeddedHandlers[tableId];
// Controleer of de form action correct is ingesteld if (typeof embedded === 'function') {
if (!form.action || form.action === '' || form.action === window.location.href || form.action === window.location.pathname) { const instance = window.EveAI.ListView.instances[tableId];
console.warn('Form action is mogelijk niet correct ingesteld:', form.action); try {
// Als er geen action is ingesteld, gebruik dan de huidige URL embedded(action, instance ? instance.selectedRow : null, tableId);
form.action = window.location.href; return true;
} catch (err) {
console.error('Embedded handler error:', err);
return false;
} }
console.log(`Form action is: ${form.action}`);
form.submit();
return true;
} }
// Fallback to global handler (legacy behavior)
if (window.EveAI && window.EveAI.ListView && typeof window.EveAI.ListView.handleAction === 'function') {
return window.EveAI.ListView.handleAction(action, requiresSelection, tableId);
}
// Final fallback to form submit (legacy)
const actionInput = document.getElementById(`${tableId}-action`);
if (actionInput) actionInput.value = action;
const form = document.getElementById(`${tableId}-form`);
if (form) { form.submit(); return true; }
return false; return false;
}; };
} }

View File

@@ -4,25 +4,51 @@
{% block content_description %}{{ description }}{% endblock %} {% block content_description %}{{ description }}{% endblock %}
{% block content %} {% block content %}
{% set disabled_fields = [] %} <form id="componentEditForm" method="post">
{% set exclude_fields = [] %} {{ form.hidden_tag() }}
{% for field in form.get_static_fields() %} {% set disabled_fields = [] %}
{{ render_field(field, disabled_fields, exclude_fields) }} {% set exclude_fields = [] %}
{% endfor %} {% for field in form.get_static_fields() %}
{% if form.get_dynamic_fields is defined %} {{ render_field(field, disabled_fields, exclude_fields) }}
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %} {% endfor %}
{% if form.get_dynamic_fields is defined %}
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %}
{% endif %}
<div class="btn-group mt-3">
{% if enable_reset_defaults %}
<button type="button" class="btn btn-outline-secondary" id="resetDefaults"
data-model-default="{{ model_default | default('') }}"
data-temperature-default="{{ temperature_default | default('') }}">Reset to defaults</button>
{% endif %}
<button type="submit" class="btn btn-primary ms-2">{{ submit_text }}</button>
<button type="button" class="btn btn-secondary ms-2" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
{% if enable_reset_defaults %}
<script>
(function(){
const btn = document.getElementById('resetDefaults');
if (!btn) return;
btn.addEventListener('click', function(){
const modelDef = btn.getAttribute('data-model-default');
const tempDef = btn.getAttribute('data-temperature-default');
const modelEl = document.querySelector('#componentEditForm select[name="llm_model"]');
const tempEl = document.querySelector('#componentEditForm input[name="temperature"]');
if (modelEl && modelDef) { modelEl.value = modelDef; }
if (tempEl && tempDef) {
tempEl.value = tempDef;
}
});
})();
</script>
{% endif %} {% endif %}
<div class="btn-group mt-3">
<button type="submit" class="btn btn-primary component-submit">{{ submit_text }}</button>
<button type="button" class="btn btn-secondary ms-2" id="cancelEdit">Cancel</button>
</div>
{% endblock %} {% endblock %}
{% block content_footer %} {% block content_footer %}

View File

@@ -32,23 +32,8 @@
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#agents-tab" role="tab"> <a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#components-tab" role="tab">
Agents Components
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#tasks-tab" role="tab">
Tasks
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#tools-tab" role="tab">
Tools
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1 d-none" id="editor-tab-link" data-bs-toggle="tab" href="#editor-tab" role="tab">
Editor
</a> </a>
</li> </li>
</ul> </ul>
@@ -63,17 +48,17 @@
{% endfor %} {% endfor %}
<!-- Overview Section --> <!-- Overview Section -->
<div class="row mb-4"> {# <div class="row mb-4">#}
<div class="col-12"> {# <div class="col-12">#}
<div class="card"> {# <div class="card">#}
<div class="card-body"> {# <div class="card-body">#}
<div class="specialist-overview" id="specialist-svg"> {# <div class="specialist-overview" id="specialist-svg">#}
<img src="{{ svg_path }}" alt="Specialist Overview" class="w-100"> {# <img src="{{ svg_path }}" alt="Specialist Overview" class="w-100">#}
</div> {# </div>#}
</div> {# </div>#}
</div> {# </div>#}
</div> {# </div>#}
</div> {# </div>#}
</div> </div>
<!-- Configuration Tab --> <!-- Configuration Tab -->
@@ -88,79 +73,28 @@
{% endfor %} {% endfor %}
</div> </div>
<!-- Agents Tab --> <!-- Components Tab (Unified list view) -->
<div class="tab-pane fade" id="agents-tab" role="tabpanel"> <div class="tab-pane fade" id="components-tab" role="tabpanel">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
{{ render_selectable_table( <div class="container">
headers=["Agent ID", "Name", "Type", "Type Version"], <input type="hidden" id="{{ components_table_id }}-selected-row" name="selected_row" value="">
rows=agent_rows if agent_rows else [], <input type="hidden" id="{{ components_table_id }}-action" name="action" value="">
selectable=True, <div id="{{ components_table_id }}" class="tabulator-list-view"></div>
id="agentsTable", <div class="row mt-3">
is_component_selector=True <div class="col-12">
) }} <button type="button"
<div class="form-group mt-3"> data-table-id="{{ components_table_id }}"
<button type="button" class="btn btn-primary edit-component" onclick="handleListViewAction('edit_component', true, event)"
data-component-type="agent" class="btn btn-primary requires-selection" disabled>
data-edit-url="{{ prefixed_url_for('interaction_bp.edit_agent', agent_id=0) }}">Edit Agent Edit
</button> </button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Tasks Tab -->
<div class="tab-pane fade" id="tasks-tab" role="tabpanel">
<div class="card">
<div class="card-body">
{{ 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
) }}
<div class="form-group mt-3">
<button type="button" class="btn btn-primary edit-component"
data-component-type="task"
data-edit-url="{{ prefixed_url_for('interaction_bp.edit_task', task_id=0) }}">Edit Task
</button>
</div>
</div>
</div>
</div>
<!-- Tools Tab -->
<div class="tab-pane fade" id="tools-tab" role="tabpanel">
<div class="card">
<div class="card-body">
{{ 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
) }}
<div class="form-group mt-3">
<button type="button" class="btn btn-primary edit-component"
data-component-type="tool"
data-edit-url="{{ prefixed_url_for('interaction_bp.edit_tool', tool_id=0) }}">Edit Tool
</button>
</div>
</div>
</div>
</div>
<!-- Editor Tab -->
<div class="tab-pane fade" id="editor-tab" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0" id="editorTitle"></h5>
</div>
<div class="card-body" id="editorContent">
<!-- Component editor will be loaded here -->
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -170,245 +104,219 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Component Editor Modal (intentionally placed outside the main form to avoid nested forms) -->
<div class="modal fade" id="componentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="componentModalLabel">Edit Component</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="componentModalBody">
<!-- Partial form will be injected here -->
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{ url_for('static', filename='assets/js/eveai-list-view.js') }}"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const editorTab = document.getElementById('editor-tab'); const componentModalEl = document.getElementById('componentModal');
const editorTabLink = document.getElementById('editor-tab-link'); const componentModalBody = document.getElementById('componentModalBody');
const editorTitle = document.getElementById('editorTitle'); const componentModalLabel = document.getElementById('componentModalLabel');
const editorContent = document.getElementById('editorContent'); let componentModal;
let previousTab = null;
// Add color classes to the tabs // Initialize the combined components list using EveAI.ListView
const agentsTabLink = document.querySelector('[href="#agents-tab"]'); function initComponentsList() {
const tasksTabLink = document.querySelector('[href="#tasks-tab"]'); function tryInit() {
const toolsTabLink = document.querySelector('[href="#tools-tab"]'); if (window.EveAI && window.EveAI.ListView && typeof window.EveAI.ListView.initialize === 'function') {
const cfg = {
data: {{ components_data | tojson }},
columns: {{ components_columns | tojson }},
initialSort: {{ components_initial_sort | tojson }},
index: {{ components_index | tojson }},
actions: {{ components_actions | tojson }},
tableHeight: {{ components_table_height|default(600) }},
selectable: true
};
// Resolve string formatters (e.g., 'typeBadge') to functions before initializing Tabulator
if (window.EveAI && window.EveAI.ListView && window.EveAI.ListView.formatters && Array.isArray(cfg.columns)) {
cfg.columns = cfg.columns.map(col => {
if (typeof col.formatter === 'string' && window.EveAI.ListView.formatters[col.formatter]) {
return { ...col, formatter: window.EveAI.ListView.formatters[col.formatter] };
}
return col;
});
}
const table = window.EveAI.ListView.initialize('{{ components_table_id }}', cfg);
// Expose for quick debugging
window.__componentsTable = table;
agentsTabLink.classList.add('component-agent'); // Fallback: ensure instance registry is populated even if the module didn't store it yet
tasksTabLink.classList.add('component-task'); window.EveAI.ListView.instances = window.EveAI.ListView.instances || {};
toolsTabLink.classList.add('component-tool'); if (!window.EveAI.ListView.instances['{{ components_table_id }}'] && table) {
window.EveAI.ListView.instances['{{ components_table_id }}'] = {
table: table,
config: cfg,
selectedRow: null
};
}
// Ensure single-click selects exactly one row (defensive)
if (table && typeof table.on === 'function') {
try {
table.on('rowClick', function(e, row){ row.getTable().deselectRow(); row.select(); });
table.on('cellClick', function(e, cell){ const r = cell.getRow(); r.getTable().deselectRow(); r.select(); });
table.on('rowSelectionChanged', function(data, rows){
const inst = window.EveAI.ListView.instances['{{ components_table_id }}'];
if (inst) { inst.selectedRow = rows.length ? rows[0].getData() : null; }
if (typeof window.EveAI.ListView.updateActionButtons === 'function') {
window.EveAI.ListView.updateActionButtons('{{ components_table_id }}');
}
});
} catch (err) { console.warn('Could not attach selection handlers:', err); }
}
// Add background colors to the tab panes // Register embedded action handler for this table
const agentsTab = document.getElementById('agents-tab'); window.EveAI.ListView.embeddedHandlers = window.EveAI.ListView.embeddedHandlers || {};
const tasksTab = document.getElementById('tasks-tab'); window.EveAI.ListView.embeddedHandlers['{{ components_table_id }}'] = function(action, row, tableId){
const toolsTab = document.getElementById('tools-tab'); if (action !== 'edit_component' || !row) return;
const id = row.id;
const type = row.type_name; // 'agent' | 'task' | 'tool'
agentsTab.classList.add('component-agent-bg'); // Build edit URL from server-side templates with placeholder 0
tasksTab.classList.add('component-task-bg'); const editUrls = {
toolsTab.classList.add('component-tool-bg'); agent: "{{ prefixed_url_for('interaction_bp.edit_agent', agent_id=0) }}",
task: "{{ prefixed_url_for('interaction_bp.edit_task', task_id=0) }}",
tool: "{{ prefixed_url_for('interaction_bp.edit_tool', tool_id=0) }}",
};
const url = (editUrls[type] || '').replace('/0', `/${id}`);
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(resp => { if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return resp.text(); })
.then(html => {
componentModalBody.innerHTML = html;
componentModalLabel.textContent = `Edit ${type.charAt(0).toUpperCase()+type.slice(1)}`;
componentModalEl.dataset.componentType = type;
componentModalEl.dataset.componentId = id;
// Ensure component selectors don't interfere with form submission // Helper to open modal once Bootstrap is available
const form = document.getElementById('specialistForm'); function openModal() {
try {
if (!componentModal) componentModal = new bootstrap.Modal(componentModalEl);
componentModal.show();
} catch (e) {
console.error('Failed to open Bootstrap modal:', e);
alert('Kan de editor niet openen (Bootstrap modal ontbreekt).');
}
}
form.addEventListener('submit', function(e) { if (window.bootstrap && typeof bootstrap.Modal === 'function') {
// Remove component selectors from form validation openModal();
const componentSelectors = form.querySelectorAll('input[data-component-selector]'); } else {
componentSelectors.forEach(selector => { // Fallback: laad Bootstrap bundle dynamisch en open daarna
selector.removeAttribute('required'); const script = document.createElement('script');
}); script.src = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js';
}); script.onload = () => openModal();
script.onerror = () => {
// Get all tab links except the editor tab console.error('Kon Bootstrap bundle niet laden');
const tabLinks = Array.from(document.querySelectorAll('.nav-link')).filter(link => link.id !== 'editor-tab-link'); alert('Kan de editor niet openen omdat Bootstrap JS ontbreekt.');
};
// Function to toggle other tabs' disabled state document.head.appendChild(script);
function toggleOtherTabs(disable) { }
tabLinks.forEach(link => { })
if (disable) { .catch(err => {
link.classList.add('disabled'); console.error('Error loading editor:', err);
alert('Error loading editor. Please try again.');
});
};
} else { } else {
link.classList.remove('disabled'); setTimeout(tryInit, 100);
} }
});
}
// Function to toggle main form elements
const mainSubmitButton = document.querySelector('#specialistForm > .btn-primary');
function toggleMainFormElements(disable) {
// Toggle tabs
document.querySelectorAll('.nav-link').forEach(link => {
if (link.id !== 'editor-tab-link') {
if (disable) {
link.classList.add('disabled');
} else {
link.classList.remove('disabled');
}
}
});
// Toggle main submit button
if (mainSubmitButton) {
mainSubmitButton.disabled = disable;
} }
tryInit();
} }
// Handle edit buttons initComponentsList();
document.querySelectorAll('.edit-component').forEach(button => {
button.addEventListener('click', function() {
const componentType = this.dataset.componentType;
const form = this.closest('form');
const selectedRow = form.querySelector('input[type="radio"]:checked');
console.log("I'm in the custom click event listener!") // Note: we removed the modal footer submit button. The partial provides its own buttons.
// Submissions are intercepted via the submit listener on componentModalBody below.
if (!selectedRow) { // Refresh the components list data after a successful save
alert('Please select a component to edit'); function refreshComponentsData() {
const url = "{{ prefixed_url_for('interaction_bp.specialist_components_data', specialist_id=specialist_id) }}";
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(resp => resp.json())
.then(payload => {
const instance = window.EveAI.ListView.instances['{{ components_table_id }}'];
if (instance && instance.table && Array.isArray(payload.data)) {
instance.table.setData(payload.data);
}
})
.catch(err => console.error('Failed to refresh components data', err));
}
// Intercept native form submit events within the modal (handles Enter key too)
componentModalBody.addEventListener('submit', function(e) {
const formEl = e.target.closest('#componentEditForm');
if (!formEl) return; // Not our form
e.preventDefault();
const formData = new FormData(formEl);
const componentType = componentModalEl.dataset.componentType;
const componentId = componentModalEl.dataset.componentId;
// Build robust, prefix-aware absolute save URL from server-side templates
const saveUrls = {
agent: "{{ prefixed_url_for('interaction_bp.save_agent', agent_id=0) }}",
task: "{{ prefixed_url_for('interaction_bp.save_task', task_id=0) }}",
tool: "{{ prefixed_url_for('interaction_bp.save_tool', tool_id=0) }}",
};
const urlTemplate = saveUrls[componentType];
const saveUrl = urlTemplate.replace('/0', `/${componentId}`);
fetch(saveUrl, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(async resp => {
const ct = resp.headers.get('Content-Type') || '';
if (resp.ok) {
// Expect JSON success
let data = null;
try { data = await resp.json(); } catch (_) {}
if (data && data.success) {
componentModal.hide();
refreshComponentsData();
return;
}
throw new Error(data && data.message ? data.message : 'Save failed');
}
// For validation errors (400), server returns HTML partial -> replace modal body
if (resp.status === 400 && ct.includes('text/html')) {
const html = await resp.text();
componentModalBody.innerHTML = html;
return; return;
} }
// Other errors
const valueMatch = selectedRow.value.match(/'value':\s*(\d+)/); let message = 'Save failed';
const selectedId = valueMatch ? valueMatch[1] : null; if (ct.includes('application/json')) {
try {
if (!selectedId) { const data = await resp.json();
console.error('Could not extract ID from value:', selectedRow.value); if (data && data.message) message = data.message;
alert('Error: Could not determine component ID'); } catch (_) {}
return;
} }
throw new Error(message + ` (HTTP ${resp.status})`);
// Make AJAX call to get component editor })
const urlTemplate = this.dataset.editUrl.replace('/0', `/${selectedId}`); .catch(err => {
fetch(urlTemplate, { console.error('Error saving component:', err);
headers: { alert(err.message || 'Error saving component');
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(html => {
// Store the current active tab
previousTab = document.querySelector('.nav-link.active');
// Update editor content
editorTitle.textContent = `Edit ${componentType.charAt(0).toUpperCase() + componentType.slice(1)}`;
editorContent.innerHTML = html;
// Apply the appropriate color class to the editor tab
editorTabLink.classList.remove('component-agent', 'component-task', 'component-tool');
editorTab.classList.remove('component-agent-bg', 'component-task-bg', 'component-tool-bg');
editorTabLink.classList.add(`component-${componentType}`);
editorTab.classList.add(`component-${componentType}-bg`);
// Disable other tabs & main form elements
toggleOtherTabs(true);
toggleMainFormElements(true)
// Show editor tab and switch to it
editorTabLink.classList.remove('d-none');
editorTabLink.click();
})
.catch(error => {
console.error('Error fetching editor:', error);
alert('Error loading editor. Please try again.');
});
}); });
// Clean up color classes when returning from editor
editorTabLink.addEventListener('hide.bs.tab', function() {
editorTabLink.classList.remove('component-agent', 'component-task', 'component-tool');
editorTab.classList.remove('component-agent-bg', 'component-task-bg', 'component-tool-bg');
});
});
// Handle component editor form submissions
editorContent.addEventListener('click', function(e) {
if (e.target && e.target.classList.contains('component-submit')) {
e.preventDefault();
console.log('Submit button clicked');
// Get all form fields from the editor content
const formData = new FormData();
editorContent.querySelectorAll('input, textarea, select').forEach(field => {
if (field.type === 'checkbox') {
formData.append(field.name, field.checked ? 'y' : 'n');
} else {
formData.append(field.name, field.value);
}
});
// Add CSRF token
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
formData.append('csrf_token', csrfToken);
// Get the component ID from the current state
const selectedRow = document.querySelector('input[name="selected_row"]:checked');
const componentData = JSON.parse(selectedRow.value.replace(/'/g, '"'));
const componentId = componentData.value;
// Determine the component type (agent, task, or tool)
const componentType = editorTabLink.classList.contains('component-agent') ? 'agent' :
editorTabLink.classList.contains('component-task') ? 'task' : 'tool';
// Submit the data
fetch(`interaction/${componentType}/${componentId}/save`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Handle success - reload the page
location.reload();
} else {
// Handle error
alert(data.message || 'Error saving component');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error saving component');
});
}
});
// Handle case when editor tab is hidden
editorTabLink.addEventListener('hide.bs.tab', function() {
// Re-enable all tabs & main form elements
toggleOtherTabs(false);
toggleMainFormElements(false)
// Remove color classes
editorTabLink.classList.remove('component-agent', 'component-task', 'component-tool');
editorTab.classList.remove('component-agent-bg', 'component-task-bg', 'component-tool-bg');
});
// Function to handle canceling edit
function cancelEdit() {
// Re-enable all tabs & main form elements
toggleOtherTabs(false);
toggleMainFormElements()
// Return to previous tab
if (previousTab) {
previousTab.click();
}
// Hide the editor tab
editorTabLink.classList.add('d-none');
}
// Handle cancel button in editor
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'cancelEdit') {
// Get the previously active tab (stored before switching to editor)
const previousTab = document.querySelector('[href="#configuration-tab"]'); // default to configuration tab
cancelEdit()
// Hide the editor tab
document.getElementById('editor-tab-link').classList.add('d-none');
}
}); });
}); });
</script> </script>

View File

@@ -122,21 +122,41 @@ function validateTableSelection(formId) {
} }
</script> </script>
<script> <script>
$(document).ready(function() { (function(){
// Maak tabelrijen klikbaar (voor tabellen met radio-buttons) function initClickableRowsWithjQuery(){
$(document).on('click', 'table tbody tr', function(e) { // Maak tabelrijen klikbaar (voor tabellen met radio-buttons)
// Voorkom dat dit gedrag optreedt als er direct op de radio-button of een link wordt geklikt $(document).on('click', 'table tbody tr', function(e) {
if (!$(e.target).is('input[type="radio"], a')) { // Voorkom dat dit gedrag optreedt als er direct op de radio-button of een link wordt geklikt
// Vind de radio-button in deze rij if (!$(e.target).is('input[type="radio"], a')) {
const radio = $(this).find('input[type="radio"]'); // Vind de radio-button in deze rij
// Selecteer de radio-button const radio = $(this).find('input[type="radio"]');
radio.prop('checked', true); // Selecteer de radio-button
// Voeg visuele feedback toe voor de gebruiker radio.prop('checked', true);
$('table tbody tr').removeClass('table-active'); // Voeg visuele feedback toe voor de gebruiker
$(this).addClass('table-active'); $('table tbody tr').removeClass('table-active');
} $(this).addClass('table-active');
}); }
}); });
}
if (window.$) {
$(document).ready(initClickableRowsWithjQuery);
} else {
// Fallback zonder jQuery: beperkte ondersteuning
document.addEventListener('click', function(e){
const row = e.target.closest('table tbody tr');
if (!row) return;
// klik op radio/link niet overrulen
if (e.target.closest('input[type="radio"], a')) return;
const radio = row.querySelector('input[type="radio"]');
if (radio) {
radio.checked = true;
// visuele feedback
row.closest('tbody')?.querySelectorAll('tr').forEach(tr => tr.classList.remove('table-active'));
row.classList.add('table-active');
}
});
}
})();
</script> </script>
<style> <style>

View File

@@ -97,7 +97,8 @@ class OrderedListField(TextAreaField):
class DynamicFormBase(FlaskForm): class DynamicFormBase(FlaskForm):
def __init__(self, formdata=None, *args, **kwargs): def __init__(self, formdata=None, *args, **kwargs):
super(DynamicFormBase, self).__init__(*args, **kwargs) # Belangrijk: formdata doorgeven aan FlaskForm zodat WTForms POST-data kan binden
super(DynamicFormBase, self).__init__(formdata=formdata, *args, **kwargs)
# Maps collection names to lists of field names # Maps collection names to lists of field names
self.dynamic_fields = {} self.dynamic_fields = {}
# Store formdata for later use # Store formdata for later use

View File

@@ -1,4 +1,4 @@
from flask import session from flask import session, current_app
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SelectField, TextAreaField) from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
from wtforms.fields.datetime import DateField from wtforms.fields.datetime import DateField
@@ -95,12 +95,73 @@ class EditEveAIAgentForm(BaseEditComponentForm):
llm_model = SelectField('LLM Model', validators=[Optional()]) llm_model = SelectField('LLM Model', validators=[Optional()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
obj = kwargs.get('obj')
agent_type = None
agent_type_version = None
current_llm_model = None
current_temperature = None
if obj:
agent_type = obj.type
agent_type_version = obj.type_version
current_llm_model = obj.llm_model
current_temperature = obj.temperature
# Bewaar flags over oorspronkelijke None-status voor optionele normalisatie in populate_obj
self._was_llm_model_none = (current_llm_model is None)
self._was_temperature_none = (current_temperature is None)
super().__init__(*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): # Choices instellen
self.llm_model.choices = agent_config.allowed_models 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')
default_temperature = self._agent_config.get('temperature', 0.7)
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)]
# Defaults alleen instellen wanneer er geen formdata is (GET render of programmatic constructie)
is_post = bool(getattr(self, 'formdata', None))
if not is_post:
if current_llm_model is None:
self.llm_model.data = full_model_name
if current_temperature is None:
self.temperature.data = default_temperature
else: 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"""
# Roep de parent populate_obj aan
current_app.logger.info(f"populate_obj called with obj: {obj}")
super().populate_obj(obj)
current_app.logger.info(f"populate_obj done with obj: {obj}")
# Als de originele waarde None was EN de nieuwe waarde gelijk is aan de config default,
# herstel dan de None waarde (alleen als het eerder None was)
if getattr(self, '_agent_config', None):
full_model_name = self._agent_config.get('full_model_name', 'mistral.mistral-medium-latest')
if self._was_llm_model_none and obj.llm_model == full_model_name:
obj.llm_model = None
default_temperature = self._agent_config.get('temperature', 0.7)
if self._was_temperature_none and obj.temperature == default_temperature:
obj.temperature = None
current_app.logger.info(f"populate_obj default check results in obj: {obj}")
class EditEveAITaskForm(BaseEditComponentForm): class EditEveAITaskForm(BaseEditComponentForm):

View File

@@ -262,23 +262,42 @@ def edit_specialist(specialist_id):
db.session.rollback() db.session.rollback()
flash(f'Failed to update specialist. Error: {str(e)}', 'danger') flash(f'Failed to update specialist. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update specialist {specialist_id}. Error: {str(e)}') current_app.logger.error(f'Failed to update specialist {specialist_id}. Error: {str(e)}')
# On error, re-render with components list config
from eveai_app.views.list_views.interaction_list_views import get_specialist_components_list_view
components_config = get_specialist_components_list_view(specialist)
return render_template('interaction/edit_specialist.html', return render_template('interaction/edit_specialist.html',
form=form, form=form,
specialist_id=specialist_id, specialist_id=specialist_id,
agent_rows=agent_rows, components_title=components_config.get('title'),
task_rows=task_rows, components_data=components_config.get('data'),
tool_rows=tool_rows, components_columns=components_config.get('columns'),
components_actions=components_config.get('actions'),
components_initial_sort=components_config.get('initial_sort'),
components_table_id=components_config.get('table_id'),
components_table_height=components_config.get('table_height'),
components_description=components_config.get('description'),
components_index=components_config.get('index'),
prefixed_url_for=prefixed_url_for, prefixed_url_for=prefixed_url_for,
svg_path=svg_path, ) svg_path=svg_path, )
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
# Build combined components list view config for embedding
from eveai_app.views.list_views.interaction_list_views import get_specialist_components_list_view
components_config = get_specialist_components_list_view(specialist)
return render_template('interaction/edit_specialist.html', return render_template('interaction/edit_specialist.html',
form=form, form=form,
specialist_id=specialist_id, specialist_id=specialist_id,
agent_rows=agent_rows, components_title=components_config.get('title'),
task_rows=task_rows, components_data=components_config.get('data'),
tool_rows=tool_rows, components_columns=components_config.get('columns'),
components_actions=components_config.get('actions'),
components_initial_sort=components_config.get('initial_sort'),
components_table_id=components_config.get('table_id'),
components_table_height=components_config.get('table_height'),
components_description=components_config.get('description'),
components_index=components_config.get('index'),
prefixed_url_for=prefixed_url_for, prefixed_url_for=prefixed_url_for,
svg_path=svg_path, ) svg_path=svg_path, )
@@ -310,6 +329,15 @@ def handle_specialist_selection():
return redirect(prefixed_url_for('interaction_bp.specialists', for_redirect=True)) return redirect(prefixed_url_for('interaction_bp.specialists', for_redirect=True))
@interaction_bp.route('/specialist/<int:specialist_id>/components_data', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialist_components_data(specialist_id):
"""Return JSON data for the specialist's combined components list (agents, tasks, tools)."""
specialist = Specialist.query.get_or_404(specialist_id)
from eveai_app.views.list_views.interaction_list_views import get_specialist_components_list_view
config = get_specialist_components_list_view(specialist)
return jsonify({'data': config.get('data', [])})
# Routes for Agent management --------------------------------------------------------------------- # Routes for Agent management ---------------------------------------------------------------------
@interaction_bp.route('/agent/<int:agent_id>/edit', methods=['GET']) @interaction_bp.route('/agent/<int:agent_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
@@ -318,22 +346,34 @@ def edit_agent(agent_id):
form = EditEveAIAgentForm(obj=agent) form = EditEveAIAgentForm(obj=agent)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Determine defaults for reset button if available
enable_reset_defaults = False
model_default = ''
temperature_default = ''
if getattr(form, '_agent_config', None):
model_default = form._agent_config.get('full_model_name', 'mistral.mistral-medium-latest')
temperature_default = form._agent_config.get('temperature', 0.7)
enable_reset_defaults = True
# Return just the form portion for AJAX requests # Return just the form portion for AJAX requests
return render_template('interaction/components/edit_agent.html', return render_template('interaction/components/edit_agent.html',
form=form, form=form,
agent=agent, agent=agent,
title="Edit Agent", title="Edit Agent",
description="Configure the agent with company-specific details if required", description="Configure the agent with company-specific details if required",
submit_text="Save Agent") submit_text="Save Agent",
enable_reset_defaults=enable_reset_defaults,
model_default=model_default,
temperature_default=temperature_default)
return None return None
@interaction_bp.route('/agent/<int:agent_id>/save', methods=['POST']) @interaction_bp.route('/agent/<int:agent_id>/save', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def save_agent(agent_id): def save_agent(agent_id):
current_app.logger.info(f'Trying to save agent {agent_id} -------------------------------------------')
agent = EveAIAgent.query.get_or_404(agent_id) if agent_id else EveAIAgent() agent = EveAIAgent.query.get_or_404(agent_id) if agent_id else EveAIAgent()
tenant_id = session.get('tenant').get('id') tenant_id = session.get('tenant').get('id')
form = EditEveAIAgentForm(obj=agent) form = EditEveAIAgentForm(formdata=request.form, obj=agent)
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
@@ -347,8 +387,25 @@ def save_agent(agent_id):
db.session.rollback() db.session.rollback()
current_app.logger.error(f'Failed to save agent {agent_id} for tenant {tenant_id}. Error: {str(e)}') current_app.logger.error(f'Failed to save agent {agent_id} for tenant {tenant_id}. Error: {str(e)}')
return jsonify({'success': False, 'message': f"Failed to save agent {agent_id}: {str(e)}"}) return jsonify({'success': False, 'message': f"Failed to save agent {agent_id}: {str(e)}"})
else:
return jsonify({'success': False, 'message': 'Validation failed'}) # On validation errors, return the editor partial HTML so the frontend can display inline errors in the modal
form_validation_failed(request, form)
enable_reset_defaults = False
model_default = ''
temperature_default = ''
if getattr(form, '_agent_config', None):
model_default = form._agent_config.get('full_model_name', 'mistral.mistral-medium-latest')
temperature_default = form._agent_config.get('temperature', 0.7)
enable_reset_defaults = True
return render_template('interaction/components/edit_agent.html',
form=form,
agent=agent,
title="Edit Agent",
description="Configure the agent with company-specific details if required",
submit_text="Save Agent",
enable_reset_defaults=enable_reset_defaults,
model_default=model_default,
temperature_default=temperature_default), 400
# Routes for Task management ---------------------------------------------------------------------- # Routes for Task management ----------------------------------------------------------------------
@@ -374,7 +431,7 @@ def edit_task(task_id):
def save_task(task_id): def save_task(task_id):
task = EveAITask.query.get_or_404(task_id) if task_id else EveAITask() task = EveAITask.query.get_or_404(task_id) if task_id else EveAITask()
tenant_id = session.get('tenant').get('id') tenant_id = session.get('tenant').get('id')
form = EditEveAITaskForm(obj=task) # Replace with actual task form form = EditEveAITaskForm(formdata=request.form, obj=task) # Bind explicit formdata
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
@@ -389,7 +446,14 @@ def save_task(task_id):
current_app.logger.error(f'Failed to save task {task_id} for tenant {tenant_id}. Error: {str(e)}') current_app.logger.error(f'Failed to save task {task_id} for tenant {tenant_id}. Error: {str(e)}')
return jsonify({'success': False, 'message': f"Failed to save task {task_id}: {str(e)}"}) return jsonify({'success': False, 'message': f"Failed to save task {task_id}: {str(e)}"})
return jsonify({'success': False, 'message': 'Validation failed'}) # On validation errors, return the editor partial HTML (400) so frontend can show inline errors
form_validation_failed(request, form)
return render_template('interaction/components/edit_task.html',
form=form,
task=task,
title="Edit Task",
description="Configure the task with company-specific details if required",
submit_text="Save Task"), 400
# Routes for Tool management ---------------------------------------------------------------------- # Routes for Tool management ----------------------------------------------------------------------
@@ -415,7 +479,7 @@ def edit_tool(tool_id):
def save_tool(tool_id): def save_tool(tool_id):
tool = EveAITool.query.get_or_404(tool_id) if tool_id else EveAITool() tool = EveAITool.query.get_or_404(tool_id) if tool_id else EveAITool()
tenant_id = session.get('tenant').get('id') tenant_id = session.get('tenant').get('id')
form = EditEveAIToolForm(obj=tool) # Replace with actual tool form form = EditEveAIToolForm(formdata=request.form, obj=tool)
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
@@ -430,7 +494,14 @@ def save_tool(tool_id):
current_app.logger.error(f'Failed to save tool {tool_id} for tenant {tenant_id}. Error: {str(e)}') current_app.logger.error(f'Failed to save tool {tool_id} for tenant {tenant_id}. Error: {str(e)}')
return jsonify({'success': False, 'message': f"Failed to save tool {tool_id}: {str(e)}"}) return jsonify({'success': False, 'message': f"Failed to save tool {tool_id}: {str(e)}"})
return jsonify({'success': False, 'message': 'Validation failed'}) # On validation errors, return the editor partial HTML (400)
form_validation_failed(request, form)
return render_template('interaction/components/edit_tool.html',
form=form,
tool=tool,
title="Edit Tool",
description="Configure the tool with company-specific details if required",
submit_text="Save Tool"), 400
# Component selection handlers -------------------------------------------------------------------- # Component selection handlers --------------------------------------------------------------------

View File

@@ -245,3 +245,74 @@ def get_eveai_data_capsules_list_view():
'table_height': 800 'table_height': 800
} }
# Combined specialist components list view helper
def get_specialist_components_list_view(specialist):
"""Generate a combined list view configuration for a specialist's agents, tasks, and tools"""
# Build unified data rows: id, name, type_name (agent|task|tool), type, type_version
data = []
# Agents
for agent in getattr(specialist, 'agents', []) or []:
data.append({
'id': agent.id,
'name': getattr(agent, 'name', f'Agent {agent.id}'),
'type_name': 'agent',
'type': agent.type,
'type_version': agent.type_version,
'row_key': f"agent:{agent.id}",
})
# Tasks
for task in getattr(specialist, 'tasks', []) or []:
data.append({
'id': task.id,
'name': getattr(task, 'name', f'Task {task.id}'),
'type_name': 'task',
'type': task.type,
'type_version': task.type_version,
'row_key': f"task:{task.id}",
})
# Tools
for tool in getattr(specialist, 'tools', []) or []:
data.append({
'id': tool.id,
'name': getattr(tool, 'name', f'Tool {tool.id}'),
'type_name': 'tool',
'type': tool.type,
'type_version': tool.type_version,
'row_key': f"tool:{tool.id}",
})
current_app.logger.debug(f'Combined specialist components list view data: \n{data}')
# Sort ascending by id as requested
data.sort(key=lambda r: (r.get('id') or 0))
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Kind', 'field': 'type_name', 'formatter': 'typeBadge'},
{'title': 'Type', 'field': 'type'},
{'title': 'Type Version', 'field': 'type_version'},
]
actions = [
{'value': 'edit_component', 'text': 'Edit', 'class': 'btn-primary', 'requiresSelection': True},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Components',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'specialist_components_table',
'description': 'Agents, Tasks, and Tools associated with this specialist',
'table_height': 600,
'index': 'row_key',
}

View File

@@ -6,6 +6,7 @@ from dataclasses import dataclass
from flask import current_app from flask import current_app
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy import text
from common.extensions import db, cache_manager from common.extensions import db, cache_manager
from common.models.interaction import ChatSession, Interaction from common.models.interaction import ChatSession, Interaction
@@ -111,6 +112,14 @@ class ChatSessionCacheHandler(CacheHandler[CachedSession]):
Note: Note:
Only adds the interaction if it has an answer 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: if not interaction.specialist_results:
return # Skip incomplete interactions return # Skip incomplete interactions

View File

@@ -351,16 +351,27 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
return response return response
except Exception as e: except Exception as e:
# Ensure DB session is usable after an error
try:
db.session.rollback()
except Exception:
pass
stacktrace = traceback.format_exc() stacktrace = traceback.format_exc()
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)}) ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
current_app.logger.error(f'execute_specialist: Error executing specialist: {e}\n{stacktrace}') current_app.logger.error(f'execute_specialist: Error executing specialist: {e}\n{stacktrace}')
new_interaction.processing_error = str(e)[:255] if new_interaction is not None:
try: new_interaction.processing_error = str(e)[:255]
db.session.add(new_interaction) try:
db.session.commit() db.session.add(new_interaction)
except SQLAlchemyError as e: db.session.commit()
stacktrace = traceback.format_exc() except SQLAlchemyError as e:
current_app.logger.error(f'execute_specialist: Error updating interaction: {e}\n{stacktrace}') # 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) self.update_state(state=states.FAILURE)
raise raise

View File

@@ -28,3 +28,40 @@ if (typeof TabulatorFull.prototype.moduleRegistered !== 'function' ||
TabulatorFull.modules.format = TabulatorFull.modules.format || {}; TabulatorFull.modules.format = TabulatorFull.modules.format || {};
TabulatorFull.modules.format.formatters = TabulatorFull.modules.format.formatters || {}; TabulatorFull.modules.format.formatters = TabulatorFull.modules.format.formatters || {};
} }
// Registreer een universele formatter 'typeBadge' zodat string-formatters altijd werken
try {
if (typeof TabulatorFull.prototype.extendModule === 'function') {
TabulatorFull.prototype.extendModule('format', 'formatters', {
typeBadge: function(cell) {
const raw = (cell.getValue() || '').toString();
const val = raw.toLowerCase();
const map = {
'agent': { cls: 'badge text-bg-primary', label: raw || 'Agent' },
'task': { cls: 'badge text-bg-warning', label: raw || 'Task' },
'tool': { cls: 'badge text-bg-info', label: raw || 'Tool' },
};
const conf = map[val] || { cls: 'badge text-bg-secondary', label: raw };
return `<span class="${conf.cls}">${conf.label}</span>`;
}
});
} else {
// Fallback voor oudere Tabulator builds zonder extendModule
TabulatorFull.modules = TabulatorFull.modules || {};
TabulatorFull.modules.format = TabulatorFull.modules.format || {};
TabulatorFull.modules.format.formatters = TabulatorFull.modules.format.formatters || {};
TabulatorFull.modules.format.formatters.typeBadge = function(cell) {
const raw = (cell.getValue() || '').toString();
const val = raw.toLowerCase();
const map = {
'agent': { cls: 'badge text-bg-primary', label: raw || 'Agent' },
'task': { cls: 'badge text-bg-warning', label: raw || 'Task' },
'tool': { cls: 'badge text-bg-info', label: raw || 'Tool' },
};
const conf = map[val] || { cls: 'badge text-bg-secondary', label: raw };
return `<span class="${conf.cls}">${conf.label}</span>`;
};
}
} catch (e) {
console.warn('Kon typeBadge formatter niet registreren:', e);
}