- Specialist Editor Change (all components in same overview), modal editors to allow for more complex configuration of Agents, Tasks and Tools

- Strengthening dynamic forms
This commit is contained in:
Josako
2025-10-28 17:35:36 +01:00
parent b3ee2f7ce9
commit d6041ebb27
13 changed files with 736 additions and 427 deletions

View File

@@ -97,7 +97,8 @@ class OrderedListField(TextAreaField):
class DynamicFormBase(FlaskForm):
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
self.dynamic_fields = {}
# Store formdata for later use

View File

@@ -98,10 +98,17 @@ class EditEveAIAgentForm(BaseEditComponentForm):
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)
@@ -111,6 +118,8 @@ class EditEveAIAgentForm(BaseEditComponentForm):
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]
@@ -124,26 +133,36 @@ class EditEveAIAgentForm(BaseEditComponentForm):
# 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
# 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:
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
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
if original_llm_model is None and self._agent_config:
# 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 obj.llm_model == full_model_name:
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):
task_description = StringField('Task Description', validators=[Optional()])

View File

@@ -262,23 +262,42 @@ def edit_specialist(specialist_id):
db.session.rollback()
flash(f'Failed to update specialist. Error: {str(e)}', 'danger')
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',
form=form,
specialist_id=specialist_id,
agent_rows=agent_rows,
task_rows=task_rows,
tool_rows=tool_rows,
components_title=components_config.get('title'),
components_data=components_config.get('data'),
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,
svg_path=svg_path, )
else:
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',
form=form,
specialist_id=specialist_id,
agent_rows=agent_rows,
task_rows=task_rows,
tool_rows=tool_rows,
components_title=components_config.get('title'),
components_data=components_config.get('data'),
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,
svg_path=svg_path, )
@@ -310,6 +329,15 @@ def handle_specialist_selection():
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 ---------------------------------------------------------------------
@interaction_bp.route('/agent/<int:agent_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
@@ -318,22 +346,34 @@ def edit_agent(agent_id):
form = EditEveAIAgentForm(obj=agent)
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 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")
submit_text="Save Agent",
enable_reset_defaults=enable_reset_defaults,
model_default=model_default,
temperature_default=temperature_default)
return None
@interaction_bp.route('/agent/<int:agent_id>/save', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
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()
tenant_id = session.get('tenant').get('id')
form = EditEveAIAgentForm(obj=agent)
form = EditEveAIAgentForm(formdata=request.form, obj=agent)
if form.validate_on_submit():
try:
@@ -347,8 +387,25 @@ def save_agent(agent_id):
db.session.rollback()
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': 'Validation failed'})
else:
# 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 ----------------------------------------------------------------------
@@ -374,7 +431,7 @@ def edit_task(task_id):
def save_task(task_id):
task = EveAITask.query.get_or_404(task_id) if task_id else EveAITask()
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():
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)}')
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 ----------------------------------------------------------------------
@@ -415,7 +479,7 @@ def edit_tool(tool_id):
def save_tool(tool_id):
tool = EveAITool.query.get_or_404(tool_id) if tool_id else EveAITool()
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():
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)}')
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 --------------------------------------------------------------------

View File

@@ -245,3 +245,74 @@ def get_eveai_data_capsules_list_view():
'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',
}