- Started addition of Assets (to e.g. handle document templates).

- To be continued (Models, first views are ready)
This commit is contained in:
Josako
2025-03-17 17:40:42 +01:00
parent a6402524ce
commit cf2201a1f7
13 changed files with 778 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import IntegerField, FloatField, BooleanField, StringField, TextAreaField, validators, ValidationError
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
validators, ValidationError)
from flask import current_app
import json
@@ -264,6 +265,7 @@ class DynamicFormBase(FlaskForm):
'string': StringField,
'text': TextAreaField,
'date': DateField,
'file': FileField,
}.get(field_type, StringField)
field_kwargs = {}

View File

@@ -1,5 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
from wtforms.fields.datetime import DateField
from wtforms.validators import DataRequired, Length, Optional
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
@@ -94,3 +95,32 @@ class EditEveAITaskForm(BaseEditComponentForm):
class EditEveAIToolForm(BaseEditComponentForm):
pass
class AddEveAIAssetForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
type = SelectField('Type', validators=[DataRequired()])
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
types_dict = cache_manager.assets_types_cache.get_types()
self.type.choices = [(key, value['name']) for key, value in types_dict.items()]
class EditEveAIAssetForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
type = SelectField('Type', validators=[DataRequired()], render_kw={'readonly': True})
type_version = StringField('Type Version', validators=[DataRequired()], render_kw={'readonly': True})
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
class EditEveAIAssetVersionForm(DynamicFormBase):
asset_name = StringField('Asset Name', validators=[DataRequired()], render_kw={'readonly': True})
asset_type = StringField('Asset Type', validators=[DataRequired()], render_kw={'readonly': True})
asset_type_version = StringField('Asset Type Version', validators=[DataRequired()], render_kw={'readonly': True})
bucket_name = StringField('Bucket Name', validators=[DataRequired()], render_kw={'readonly': True})

View File

@@ -6,12 +6,15 @@ from flask_security import roles_accepted
from langchain.agents import Agent
from sqlalchemy import desc
from sqlalchemy.exc import SQLAlchemyError
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename
from common.models.document import Embedding, DocumentVersion, Retriever
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
EveAIAgent, EveAITask, EveAITool)
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
from common.extensions import db, cache_manager
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
from common.utils.model_logging_utils import set_logging_information, update_logging_information
from common.utils.middleware import mw_before_request
@@ -22,7 +25,7 @@ from common.utils.specialist_utils import initialize_specialist
from config.type_defs.specialist_types import SPECIALIST_TYPES
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
EditEveAIToolForm)
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm)
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
@@ -37,6 +40,7 @@ def log_after_request(response):
return response
# Routes for Chat Session Management --------------------------------------------------------------
@interaction_bp.before_request
def before_request():
try:
@@ -88,13 +92,13 @@ def view_chat_session(chat_session_id):
.filter_by(chat_session_id=chat_session.id)
.join(Specialist, Interaction.specialist_id == Specialist.id, isouter=True)
.add_columns(
Interaction.id,
Interaction.question_at,
Interaction.specialist_arguments,
Interaction.specialist_results,
Specialist.name.label('specialist_name'),
Specialist.type.label('specialist_type')
).order_by(Interaction.question_at).all())
Interaction.id,
Interaction.question_at,
Interaction.specialist_arguments,
Interaction.specialist_results,
Specialist.name.label('specialist_name'),
Specialist.type.label('specialist_type')
).order_by(Interaction.question_at).all())
# Fetch all related embeddings for the interactions in this session
embedding_query = (db.session.query(InteractionEmbedding.interaction_id,
@@ -129,6 +133,7 @@ def show_chat_session(chat_session):
return render_template('interaction/view_chat_session.html', chat_session=chat_session, interactions=interactions)
# Routes for Specialist Management ----------------------------------------------------------------
@interaction_bp.route('/specialist', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin')
def specialist():
@@ -142,7 +147,8 @@ def specialist():
# Populate fields individually instead of using populate_obj (gives problem with QueryMultipleSelectField)
new_specialist.name = form.name.data
new_specialist.type = form.type.data
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(new_specialist.type)
new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(
new_specialist.type)
new_specialist.tuning = form.tuning.data
set_logging_information(new_specialist, dt.now(tz.utc))
@@ -252,7 +258,7 @@ def edit_specialist(specialist_id):
task_rows=task_rows,
tool_rows=tool_rows,
prefixed_url_for=prefixed_url_for,
svg_path=svg_path,)
svg_path=svg_path, )
else:
form_validation_failed(request, form)
@@ -263,7 +269,7 @@ def edit_specialist(specialist_id):
task_rows=task_rows,
tool_rows=tool_rows,
prefixed_url_for=prefixed_url_for,
svg_path=svg_path,)
svg_path=svg_path, )
@interaction_bp.route('/specialists', methods=['GET', 'POST'])
@@ -298,7 +304,7 @@ def handle_specialist_selection():
return redirect(prefixed_url_for('interaction_bp.specialists'))
# Routes for Agent management
# Routes for Agent management ---------------------------------------------------------------------
@interaction_bp.route('/agent/<int:agent_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_agent(agent_id):
@@ -338,7 +344,7 @@ def save_agent(agent_id):
return jsonify({'success': False, 'message': 'Validation failed'})
# Routes for Task management
# Routes for Task management ----------------------------------------------------------------------
@interaction_bp.route('/task/<int:task_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_task(task_id):
@@ -374,7 +380,7 @@ def save_task(task_id):
return jsonify({'success': False, 'message': 'Validation failed'})
# Routes for Tool management
# Routes for Tool management ----------------------------------------------------------------------
@interaction_bp.route('/tool/<int:tool_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_tool(tool_id):
@@ -410,7 +416,7 @@ def save_tool(tool_id):
return jsonify({'success': False, 'message': 'Validation failed'})
# Component selection handlers
# Component selection handlers --------------------------------------------------------------------
@interaction_bp.route('/handle_agent_selection', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def handle_agent_selection():
@@ -447,4 +453,93 @@ def handle_tool_selection():
if action == "edit_tool":
return redirect(prefixed_url_for('interaction_bp.edit_tool', tool_id=tool_id))
return redirect(prefixed_url_for('interaction_bp.edit_specialist'))
return redirect(prefixed_url_for('interaction_bp.edit_specialist'))
# Routes for Asset management ---------------------------------------------------------------------
@interaction_bp.route('/add_asset', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin')
def add_asset():
form = AddEveAIAssetForm(request.form)
tenant_id = session.get('tenant').get('id')
if form.validate_on_submit():
try:
current_app.logger.info(f"Adding asset for tenant {tenant_id}")
api_input = {
'name': form.name.data,
'description': form.description.data,
'type': form.type.data,
'valid_from': form.valid_from.data,
'valid_to': form.valid_to.data,
}
new_asset, new_asset_version = create_asset_stack(api_input, tenant_id)
return redirect(prefixed_url_for('interaction_bp.edit_asset_version',
asset_version_id=new_asset_version.id))
except Exception as e:
current_app.logger.error(f'Failed to add asset for tenant {tenant_id}: {str(e)}')
flash('An error occurred while adding asset', 'error')
return render_template('interaction/add_asset.html')
@interaction_bp.route('/edit_asset_version/<int:asset_version_id>', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_asset_version(asset_version_id):
asset_version = EveAIAssetVersion.query.get_or_404(asset_version_id)
form = EditEveAIAssetVersionForm(asset_version)
asset_config = cache_manager.assets_config_cache.get_config(asset_version.asset.type,
asset_version.asset.type_version)
configuration_config = asset_config.get('configuration')
form.add_dynamic_fields("configuration", configuration_config, asset_version.configuration)
if form.validate_on_submit():
# Update the configuration dynamic fields
configuration = form.get_dynamic_data("configuration")
processed_configuration = {}
tenant_id = session.get('tenant').get('id')
# if files are returned, we will store the file_names in the configuration, and add the file to the appropriate
# bucket, in the appropriate location
for field_name, field_value in configuration.items():
# Handle file field - check if the value is a FileStorage instance
if isinstance(field_value, FileStorage) and field_value.filename:
try:
# Upload file and retrieve object_name for the file
object_name = add_asset_version_file(asset_version, field_name, field_value, tenant_id)
# Store object reference in configuration instead of file content
processed_configuration[field_name] = object_name
except Exception as e:
current_app.logger.error(f"Failed to upload file for asset version {asset_version.id}: {str(e)}")
flash(f"Failed to upload file '{field_value.filename}': {str(e)}", "danger")
return render_template('interaction/edit_asset_version.html', form=form,
asset_version=asset_version)
# Handle normal fields
else:
processed_configuration[field_name] = field_value
# Update the asset version with processed configuration
asset_version.configuration = processed_configuration
# Update logging information
update_logging_information(asset_version, dt.now(tz.utc))
try:
db.session.commit()
flash('Asset uploaded successfully!', 'success')
current_app.logger.info(f'Asset Version {asset_version.id} updated successfully')
return redirect(prefixed_url_for('interaction_bp.assets'))
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to upload asset. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update asset version {asset_version.id}. Error: {str(e)}')
return render_template('interaction/edit_asset_version.html', form=form)
else:
form_validation_failed(request, form)
return render_template('interaction/edit_asset_version.html', form=form)