- Adding functionality for listing and editing assets

- Started adding functionality for creating a 'full_documents' list view.
This commit is contained in:
Josako
2025-07-03 11:14:10 +02:00
parent 51d029d960
commit 50773fe602
15 changed files with 882 additions and 19 deletions

View File

@@ -15,12 +15,11 @@ from config.type_defs.processor_types import PROCESSOR_TYPES
from .config_field_types import normalize_json_field from .config_field_types import normalize_json_field
from .eveai_exceptions import (EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType, from .eveai_exceptions import (EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType,
EveAIInvalidCatalog, EveAIInvalidDocument, EveAIInvalidDocumentVersion, EveAIException) EveAIInvalidCatalog, EveAIInvalidDocument, EveAIInvalidDocumentVersion, EveAIException)
from .minio_utils import MIB_CONVERTOR
from ..models.user import Tenant from ..models.user import Tenant
from common.utils.model_logging_utils import set_logging_information, update_logging_information from common.utils.model_logging_utils import set_logging_information, update_logging_information
from common.services.entitlements import LicenseUsageServices from common.services.entitlements import LicenseUsageServices
MB_CONVERTOR = 1_048_576
def get_file_size(file): def get_file_size(file):
try: try:
@@ -39,7 +38,7 @@ def get_file_size(file):
def create_document_stack(api_input, file, filename, extension, tenant_id): def create_document_stack(api_input, file, filename, extension, tenant_id):
# Precheck if we can add a document to the stack # Precheck if we can add a document to the stack
LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, get_file_size(file)/MB_CONVERTOR) LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, get_file_size(file) / MIB_CONVERTOR)
# Create the Document # Create the Document
catalog_id = int(api_input.get('catalog_id')) catalog_id = int(api_input.get('catalog_id'))
@@ -144,7 +143,7 @@ def upload_file_for_version(doc_vers, file, extension, tenant_id):
) )
doc_vers.bucket_name = bn doc_vers.bucket_name = bn
doc_vers.object_name = on doc_vers.object_name = on
doc_vers.file_size = size / MB_CONVERTOR # Convert bytes to MB doc_vers.file_size = size / MIB_CONVERTOR # Convert bytes to MB
db.session.commit() db.session.commit()
current_app.logger.info(f'Successfully saved document to MinIO for tenant {tenant_id} for ' current_app.logger.info(f'Successfully saved document to MinIO for tenant {tenant_id} for '
@@ -354,7 +353,7 @@ def refresh_document_with_content(doc_id: int, tenant_id: int, file_content: byt
old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first() old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first()
# Precheck if we have enough quota for the new version # Precheck if we have enough quota for the new version
LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, get_file_size(file_content) / MB_CONVERTOR) LicenseUsageServices.check_storage_and_embedding_quota(tenant_id, get_file_size(file_content) / MIB_CONVERTOR)
# Create new version with same file type as original # Create new version with same file type as original
extension = old_doc_vers.file_type extension = old_doc_vers.file_type

View File

@@ -4,6 +4,9 @@ from flask import Flask
import io import io
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
MIB_CONVERTOR = 1_048_576
class MinioClient: class MinioClient:
def __init__(self): def __init__(self):
self.client = None self.client = None
@@ -58,7 +61,7 @@ class MinioClient:
raise Exception(f"Error occurred while uploading file: {err}") raise Exception(f"Error occurred while uploading file: {err}")
def upload_asset_file(self, tenant_id: int, asset_id: int, asset_type: str, file_type: str, def upload_asset_file(self, tenant_id: int, asset_id: int, asset_type: str, file_type: str,
file_data: bytes | FileStorage | io.BytesIO | str,) -> tuple[str, str, int]: file_data: bytes | FileStorage | io.BytesIO | str, ) -> tuple[str, str, int]:
bucket_name = self.generate_bucket_name(tenant_id) bucket_name = self.generate_bucket_name(tenant_id)
object_name = self.generate_asset_name(asset_id, asset_type, file_type) object_name = self.generate_asset_name(asset_id, asset_type, file_type)
@@ -120,3 +123,10 @@ class MinioClient:
self.client.remove_object(bucket_name, object_name) self.client.remove_object(bucket_name, object_name)
except S3Error as err: except S3Error as err:
raise Exception(f"Error occurred while deleting object: {err}") raise Exception(f"Error occurred while deleting object: {err}")
def get_bucket_size(self, tenant_id: int) -> int:
bucket_name = self.generate_bucket_name(tenant_id)
total_size = 0
for obj in self.client.list_objects(bucket_name, recursive=True):
total_size += obj.size
return total_size

View File

@@ -0,0 +1,114 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination, render_filter_field, render_date_filter_field, render_collapsible_section, render_selectable_sortable_table_with_dict_headers %}
{% block title %}Complete Document Overview{% endblock %}
{% block content_title %}Complete Document Overview{% endblock %}
{% block content_description %}View Documents with Latest Version for Catalog <b>{% if session.catalog_name %}{{ session.catalog_name }}{% else %}No Catalog{% endif %}</b>{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<!-- Filter Form -->
{% set filter_form %}
<form method="GET" action="{{ url_for('document_bp.full_documents') }}">
{{ render_filter_field('validity', 'Validity', filter_options['validity'], filters.get('validity', [])) }}
{{ render_filter_field('file_type', 'File Type', filter_options['file_type'], filters.get('file_type', [])) }}
{{ render_filter_field('processing', 'Processing Status', filter_options['processing'], filters.get('processing', [])) }}
{{ render_filter_field('processing_error', 'Error Status', filter_options['processing_error'], filters.get('processing_error', [])) }}
{{ render_date_filter_field('start_date', 'Processing Start Date', filters.get('start_date', [])) }}
{{ render_date_filter_field('end_date', 'Processing End Date', filters.get('end_date', [])) }}
<button type="submit" class="btn btn-primary">Apply Filters</button>
</form>
{% endset %}
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
<div class="form-group mt-3">
<form method="POST" action="{{ url_for('document_bp.handle_full_document_selection') }}" id="fullDocumentsForm">
<!-- Hidden field to store the selected version ID -->
<input type="hidden" name="version_id" id="selectedVersionId" value="">
<!-- Documents Table -->
{{ render_selectable_sortable_table_with_dict_headers(
headers=[
{"text": "Document ID", "sort": "id"},
{"text": "Name", "sort": "name"},
{"text": "Valid From", "sort": "valid_from"},
{"text": "Valid To", "sort": "valid_to"},
{"text": "Version ID", "sort": ""},
{"text": "File Type", "sort": "file_type"},
{"text": "Processing", "sort": "processing"},
{"text": "Error", "sort": "processing_error"}
],
rows=rows,
selectable=True,
id="fullDocumentsTable",
sort_by=sort_by,
sort_order=sort_order
) }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_document" class="btn btn-primary" onclick="return validateTableSelection('fullDocumentsForm')">Edit Document</button>
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('fullDocumentsForm')">Edit Document Version</button>
<button type="submit" name="action" value="document_versions" class="btn btn-secondary" onclick="return validateTableSelection('fullDocumentsForm')">Show All Document Versions</button>
<button type="submit" name="action" value="refresh_document" class="btn btn-secondary" onclick="return validateTableSelection('fullDocumentsForm')">Refresh Document (new version)</button>
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('fullDocumentsForm')">View Processed Document</button>
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('fullDocumentsForm')">Process Document Version</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.full_documents') }}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('fullDocumentsTable');
const headers = table.querySelectorAll('th.sortable');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
let sortOrder = 'asc';
if (this.querySelector('.fa-sort-up')) {
sortOrder = 'desc';
} else if (this.querySelector('.fa-sort-down')) {
sortOrder = 'none';
}
window.location.href = updateQueryStringParameter(window.location.href, 'sort_by', sortBy);
window.location.href = updateQueryStringParameter(window.location.href, 'sort_order', sortOrder);
});
});
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
table.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
var selectedRow = event.target.closest('tr');
var documentId = selectedRow.cells[1].textContent;
var versionId = selectedRow.cells[5].textContent;
console.log('Selected Document ID:', documentId, 'Version ID:', versionId);
// Update the hidden field with the version ID
document.getElementById('selectedVersionId').value = versionId;
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination, render_filter_field, render_date_filter_field, render_collapsible_section, render_selectable_sortable_table_with_dict_headers %}
{% block title %}Assets{% endblock %}
{% block content_title %}Assets{% endblock %}
{% block content_description %}View Assets{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<!-- Filter Form -->
{% set filter_form %}
<form method="GET" action="{{ url_for('interaction_bp.assets') }}">
{{ render_filter_field('type', 'Type', filter_options['type'], filters.get('type', [])) }}
{{ render_filter_field('file_type', 'Bestandstype', filter_options['file_type'], filters.get('file_type', [])) }}
<button type="submit" class="btn btn-primary">Apply Filters</button>
</form>
{% endset %}
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
<div class="form-group mt-3">
<form method="POST" action="{{ url_for('interaction_bp.handle_asset_selection') }}" id="assetsForm">
<!-- Assets Table -->
{{ render_selectable_sortable_table_with_dict_headers(
headers=[
{"text": "ID", "sort": "id"},
{"text": "Naam", "sort": "name"},
{"text": "Type", "sort": "type"},
{"text": "Type Versie", "sort": "type_version"},
{"text": "Bestandstype", "sort": "file_type"},
{"text": "Laatst Gebruikt", "sort": "last_used_at"}
],
rows=rows,
selectable=True,
id="assetsTable",
sort_by=sort_by,
sort_order=sort_order
) }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_asset" class="btn btn-primary" onclick="return validateTableSelection('assetsForm')">Edit Asset</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'interaction_bp.assets') }}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('assetsTable');
const headers = table.querySelectorAll('th.sortable');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
let sortOrder = 'asc';
if (this.querySelector('.fa-sort-up')) {
sortOrder = 'desc';
} else if (this.querySelector('.fa-sort-down')) {
sortOrder = 'none';
}
window.location.href = updateQueryStringParameter(window.location.href, 'sort_by', sortBy);
window.location.href = updateQueryStringParameter(window.location.href, 'sort_order', sortOrder);
});
});
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
table.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
var selectedRow = event.target.closest('tr');
var assetId = selectedRow.cells[1].textContent;
console.log('Selected Asset ID:', assetId);
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,201 @@
{% extends 'base.html' %}
{% block title %}Edit Asset{% endblock %}
{% block content_title %}Edit Asset{% endblock %}
{% block content_description %}Edit Asset: <b>{{ asset.name }}</b>{% endblock %}
{% block content_class %}<div class="col-xl-10 col-lg-8 col-md-10 mx-auto"></div>{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-md-8">
<h4 class="card-title">Asset Details</h4>
</div>
</div>
</div>
<div class="card-body">
<!-- Asset Information -->
<div class="row mb-4">
<div class="col-md-6">
<p><strong>ID:</strong> {{ asset.id }}</p>
<p><strong>Name:</strong> {{ asset.name }}</p>
<p><strong>Type:</strong> {{ asset.type }}</p>
</div>
<div class="col-md-6">
<p><strong>Type Version:</strong> {{ asset.type_version }}</p>
<p><strong>File Type:</strong> {{ asset.file_type }}</p>
<p><strong>File Size:</strong> {{ asset.file_size or 'N/A' }} bytes</p>
</div>
</div>
<hr>
<!-- JSON Editor Form -->
<form method="POST" id="editAssetForm">
<div class="row">
<div class="col-12">
<h5>JSON Content</h5>
<!-- JSON Editor - gebruik het eveai_json_editor patroon -->
<div class="form-group">
<textarea name="json_content" id="json_content" class="json-editor" style="display: none;">{{ json_content }}</textarea>
<div id="json_content-editor" class="json-editor-container" style="height: 500px; border: 1px solid #ddd; border-radius: 5px;"></div>
</div>
</div>
</div>
<hr>
<!-- Action Buttons -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between">
<div>
<a href="{{ url_for('interaction_bp.assets') }}" class="btn btn-secondary">
<i class="material-icons">cancel</i> Annuleren
</a>
</div>
<div>
<button type="submit" class="btn btn-primary">
<i class="material-icons">save</i> Opslaan
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const textareaElement = document.getElementById('json_content');
let currentEditor = null;
// Wacht even en probeer dan de editor te krijgen via de EveAI namespace
setTimeout(function() {
currentEditor = window.EveAI?.JsonEditors?.get('json_content-editor');
if (currentEditor) {
console.log('JSON Editor gevonden en gekoppeld');
} else {
console.log('JSON Editor nog niet beschikbaar, probeer handmatige initialisatie');
// Probeer handmatige initialisatie als fallback
if (window.EveAI?.JsonEditors?.initialize) {
try {
const initialContent = JSON.parse(textareaElement.value);
currentEditor = window.EveAI.JsonEditors.initialize('json_content-editor', initialContent, {
mode: 'tree',
readOnly: false,
mainMenuBar: true,
navigationBar: false,
statusBar: true,
onChange: (updatedContent, previousContent, { contentErrors, patchResult }) => {
console.log('Editor content changed');
// Automatisch de textarea updaten bij wijzigingen
syncEditorToTextarea();
}
});
} catch (e) {
console.error('Error bij handmatige initialisatie:', e);
}
}
}
}, 500);
// Functie om editor inhoud naar textarea te synchroniseren
function syncEditorToTextarea() {
if (currentEditor && currentEditor.get) {
try {
const content = currentEditor.get();
if (content.json !== undefined) {
textareaElement.value = JSON.stringify(content.json, null, 2);
} else if (content.text !== undefined) {
textareaElement.value = content.text;
}
console.log('Editor content gesynchroniseerd naar textarea');
} catch (e) {
console.error('Error bij synchronisatie:', e);
}
}
}
// Sync knop
document.getElementById('getEditorContentBtn').addEventListener('click', function() {
syncEditorToTextarea();
alert('Editor inhoud gesynchroniseerd naar textarea');
});
// Validate JSON button
document.getElementById('validateJsonBtn').addEventListener('click', function() {
// Eerst synchroniseren
syncEditorToTextarea();
try {
const content = textareaElement.value;
JSON.parse(content);
// Show success message
if (typeof Swal !== 'undefined') {
Swal.fire({
title: 'Geldig JSON',
text: 'De JSON syntax is correct!',
icon: 'success',
timer: 2000,
showConfirmButton: false
});
} else {
alert('De JSON syntax is correct!');
}
} catch (e) {
// Show error message
if (typeof Swal !== 'undefined') {
Swal.fire({
title: 'Ongeldig JSON',
text: 'JSON syntax fout: ' + e.message,
icon: 'error',
confirmButtonText: 'OK'
});
} else {
alert('JSON syntax fout: ' + e.message);
}
}
});
// Form submission validation
document.getElementById('editAssetForm').addEventListener('submit', function(e) {
// Eerst de editor content synchroniseren
syncEditorToTextarea();
try {
const content = textareaElement.value;
JSON.parse(content);
// JSON is valid, allow submission
return true;
} catch (error) {
e.preventDefault();
if (typeof Swal !== 'undefined') {
Swal.fire({
title: 'Ongeldig JSON',
text: 'Kan het formulier niet verzenden: JSON syntax fout - ' + error.message,
icon: 'error',
confirmButtonText: 'OK'
});
} else {
alert('Kan het formulier niet verzenden: JSON syntax fout - ' + error.message);
}
return false;
}
});
});
</script>
{% endblock %}

View File

@@ -100,6 +100,7 @@
{'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Documents', 'url': '/document/documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Documents', 'url': '/document/documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Full Documents', 'url': '/document/full_documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Document Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Document Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }} ]) }}
@@ -108,6 +109,7 @@
{{ dropdown('Interactions', 'hub', [ {{ dropdown('Interactions', 'hub', [
{'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Specialist Magic Links', 'url': '/interaction/specialist_magic_links', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Specialist Magic Links', 'url': '/interaction/specialist_magic_links', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Assets', 'url': '/interaction/assets', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }} ]) }}
{% endif %} {% endif %}

View File

@@ -18,16 +18,16 @@ from common.utils.document_utils import create_document_stack, start_embedding_t
edit_document, \ edit_document, \
edit_document_version, refresh_document, clean_url, is_file_type_supported_by_catalog edit_document_version, refresh_document, clean_url, is_file_type_supported_by_catalog
from common.utils.dynamic_field_utils import create_default_config_from_type_config from common.utils.dynamic_field_utils import create_default_config_from_type_config
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \ from common.utils.eveai_exceptions import EveAIException
EveAIDoubleURLException, EveAIException
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \ from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \
CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm
from common.utils.middleware import mw_before_request from common.utils.middleware import mw_before_request
from common.utils.celery_utils import current_celery from common.utils.celery_utils import current_celery
from common.utils.nginx_utils import prefixed_url_for from common.utils.nginx_utils import prefixed_url_for
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
from .document_list_view import DocumentListView from eveai_app.views.list_views.document_list_view import DocumentListView
from .document_version_list_view import DocumentVersionListView from eveai_app.views.list_views.document_version_list_view import DocumentVersionListView
from eveai_app.views.list_views.full_document_list_view import FullDocumentListView
document_bp = Blueprint('document_bp', __name__, url_prefix='/document') document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
@@ -499,6 +499,18 @@ def documents():
return view.get() return view.get()
@document_bp.route('/full_documents', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def full_documents():
catalog_id = session.get('catalog_id', None)
if not catalog_id:
flash('You need to set a Session Catalog before viewing Full Documents', 'warning')
return redirect(prefixed_url_for('document_bp.catalogs'))
view = FullDocumentListView(Document, 'document/full_documents.html', per_page=10)
return view.get()
@document_bp.route('/handle_document_selection', methods=['POST']) @document_bp.route('/handle_document_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_document_selection(): def handle_document_selection():
@@ -665,6 +677,59 @@ def handle_document_version_selection():
return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_vers.doc_id)) return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_vers.doc_id))
@document_bp.route('/handle_full_document_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_full_document_selection():
selected_row = request.form['selected_row']
action = request.form['action']
try:
# Parse the selected row to get document ID (first column) and version ID (fifth column)
row_data = ast.literal_eval(selected_row)
selected_doc_id = row_data.get('value')
# We need to retrieve the corresponding row data to get the version ID
# This is a bit complex with the current structure, so we'll use a different approach
if action in ['edit_document', 'document_versions', 'refresh_document']:
# Actions that need document ID
match action:
case 'edit_document':
return redirect(prefixed_url_for('document_bp.edit_document_view', document_id=selected_doc_id))
case 'document_versions':
return redirect(prefixed_url_for('document_bp.document_versions', document_id=selected_doc_id))
case 'refresh_document':
refresh_document_view(selected_doc_id)
return redirect(prefixed_url_for('document_bp.full_documents'))
else:
# Actions that need version ID
# We need to get the version ID from the selected row in the table
# We'll extract it from the form data and the version ID is in the 5th cell (index 4)
version_id_cell = int(request.form.get('version_id', 0))
# If we couldn't get a version ID, try to find the latest version for this document
if not version_id_cell:
doc_version = DocumentVersion.query.filter_by(doc_id=selected_doc_id).order_by(desc(DocumentVersion.id)).first()
if doc_version:
version_id_cell = doc_version.id
else:
flash('No document version found for this document.', 'error')
return redirect(prefixed_url_for('document_bp.full_documents'))
match action:
case 'edit_document_version':
return redirect(prefixed_url_for('document_bp.edit_document_version_view', document_version_id=version_id_cell))
case 'process_document_version':
process_version(version_id_cell)
return redirect(prefixed_url_for('document_bp.full_documents'))
case 'view_document_version_markdown':
return redirect(prefixed_url_for('document_bp.view_document_version_markdown', document_version_id=version_id_cell))
except (ValueError, AttributeError, KeyError) as e:
current_app.logger.error(f"Error processing full document selection: {str(e)}")
flash('Invalid selection or action. Please try again.', 'error')
return redirect(prefixed_url_for('document_bp.full_documents'))
@document_bp.route('/library_operations', methods=['GET', 'POST']) @document_bp.route('/library_operations', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def library_operations(): def library_operations():

View File

@@ -3,9 +3,10 @@ import json
import uuid import uuid
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
import time import time
from common.extensions import minio_client
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for
from flask_security import roles_accepted from flask_security import roles_accepted, current_user
from langchain.agents import Agent from langchain.agents import Agent
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -775,3 +776,114 @@ def handle_specialist_magic_link_selection():
specialist_magic_link_id=specialist_ml_id)) specialist_magic_link_id=specialist_ml_id))
return redirect(prefixed_url_for('interaction_bp.specialists')) return redirect(prefixed_url_for('interaction_bp.specialists'))
# Routes for Asset Management ---------------------------------------------------------------------
@interaction_bp.route('/assets', methods=['GET', 'POST'])
def assets():
from eveai_app.views.list_views.assets_list_view import AssetsListView
view = AssetsListView(
model=EveAIAsset,
template='interaction/assets.html',
per_page=10
)
return view.get()
@interaction_bp.route('/handle_asset_selection', methods=['POST'])
def handle_asset_selection():
action = request.form.get('action')
asset_id = request.form.get('selected_row')
current_app.logger.debug(f"Action: {action}, Asset ID: {asset_id}")
if action == 'edit_asset':
return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id))
return redirect(prefixed_url_for('interaction_bp.assets'))
@interaction_bp.route('/edit_asset/<int:asset_id>', methods=['GET', 'POST'])
def edit_asset(asset_id):
asset = EveAIAsset.query.get_or_404(asset_id)
tenant_id = session.get('tenant', {}).get('id')
if not tenant_id:
flash('Geen tenant geselecteerd', 'error')
return redirect(url_for('interaction_bp.assets'))
# Controleer of het bestandstype wordt ondersteund
if asset.file_type != 'json':
flash(
f'Bestandstype "{asset.file_type}" wordt momenteel niet ondersteund voor bewerking. Alleen JSON-bestanden kunnen worden bewerkt.',
'warning')
return redirect(url_for('interaction_bp.assets'))
if request.method == 'GET':
try:
# Haal het bestand op uit MinIO
file_data = minio_client.download_asset_file(
tenant_id,
asset.bucket_name,
asset.object_name
)
# Decodeer JSON data
json_content = json.loads(file_data.decode('utf-8'))
context = {
'asset': asset,
'json_content': json.dumps(json_content, indent=2),
'asset_id': asset_id
}
return render_template('interaction/edit_asset.html', **context)
except json.JSONDecodeError:
flash('Fout bij het laden van het JSON-bestand: ongeldig JSON-formaat', 'error')
return redirect(prefixed_url_for('interaction_bp.assets'))
except Exception as e:
current_app.logger.error(f"Error loading asset {asset_id}: {str(e)}")
flash(f'Fout bij het laden van het asset: {str(e)}', 'error')
return redirect(prefixed_url_for('interaction_bp.assets'))
elif request.method == 'POST':
try:
# Haal de bewerkte JSON data op uit het formulier
json_data = request.form.get('json_content')
if not json_data:
flash('Geen JSON data ontvangen', 'error')
return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id))
# Valideer JSON formaat
try:
parsed_json = json.loads(json_data)
except json.JSONDecodeError as e:
flash(f'Ongeldig JSON-formaat: {str(e)}', 'error')
return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id))
# Upload de bijgewerkte JSON naar MinIO
bucket_name, object_name, file_size = minio_client.upload_asset_file(
tenant_id,
asset.id,
asset.type,
asset.file_type,
json_data
)
# Update asset metadata
asset.file_size = file_size
asset.updated_at = dt.now(tz.utc)
asset.updated_by = current_user.id
asset.last_used_at = dt.now(tz.utc)
db.session.commit()
flash('Asset succesvol bijgewerkt', 'success')
return redirect(prefixed_url_for('interaction_bp.assets'))
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error saving asset {asset_id}: {str(e)}")
flash(f'Fout bij het opslaan van het asset: {str(e)}', 'error')
return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id))

View File

View File

@@ -0,0 +1,84 @@
from datetime import datetime as dt, timezone as tz
from flask import request, render_template, current_app
from sqlalchemy import desc, asc
from common.models.interaction import EveAIAsset
from eveai_app.views.list_views.filtered_list_view import FilteredListView
class AssetsListView(FilteredListView):
allowed_filters = ['type', 'file_type']
allowed_sorts = ['id', 'last_used_at']
def get_query(self):
return EveAIAsset.query
def apply_filters(self, query):
filters = request.args.to_dict(flat=False)
if 'type' in filters and filters['type']:
query = query.filter(EveAIAsset.type.in_(filters['type']))
if 'file_type' in filters and filters['file_type']:
query = query.filter(EveAIAsset.file_type.in_(filters['file_type']))
return query
def apply_sorting(self, query):
sort_by = request.args.get('sort_by', 'id')
sort_order = request.args.get('sort_order', 'asc')
if sort_by in self.allowed_sorts:
column = getattr(EveAIAsset, sort_by)
if sort_order == 'asc':
query = query.order_by(asc(column))
elif sort_order == 'desc':
query = query.order_by(desc(column))
return query
def get(self):
query = self.get_query()
query = self.apply_filters(query)
query = self.apply_sorting(query)
pagination = self.paginate(query)
def format_date(date):
if isinstance(date, dt):
return date.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(date, str):
return date
else:
return ''
current_app.logger.debug(f"Assets retrieved: {pagination.items}")
rows = [
[
{'value': item.id, 'class': '', 'type': 'text'},
{'value': item.name, 'class': '', 'type': 'text'},
{'value': item.type, 'class': '', 'type': 'text'},
{'value': item.type_version, 'class': '', 'type': 'text'},
{'value': item.file_type or '', 'class': '', 'type': 'text'},
{'value': format_date(item.last_used_at), 'class': '', 'type': 'text'}
] for item in pagination.items
]
context = {
'rows': rows,
'pagination': pagination,
'filters': request.args.to_dict(flat=False),
'sort_by': request.args.get('sort_by', 'id'),
'sort_order': request.args.get('sort_order', 'asc'),
'filter_options': self.get_filter_options()
}
return render_template(self.template, **context)
def get_filter_options(self):
# Haal unieke waarden op voor filters
types = [t[0] for t in EveAIAsset.query.with_entities(EveAIAsset.type).distinct().all() if t[0]]
file_types = [f[0] for f in EveAIAsset.query.with_entities(EveAIAsset.file_type).distinct().all() if f[0]]
return {
'type': [(t, t) for t in types],
'file_type': [(f, f) for f in file_types]
}

View File

@@ -1,9 +1,8 @@
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
from flask import request, render_template, session, current_app from flask import request, render_template, session, current_app
from sqlalchemy import desc, asc, or_, and_, cast, Integer from sqlalchemy import desc, asc, or_, and_
from common.models.document import Document, Catalog from common.models.document import Document
from common.utils.filtered_list_view import FilteredListView from eveai_app.views.list_views.filtered_list_view import FilteredListView
from common.utils.view_assistants import prepare_table_for_macro
class DocumentListView(FilteredListView): class DocumentListView(FilteredListView):

View File

@@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from flask import request, render_template, session from flask import request, render_template
from sqlalchemy import desc, asc from sqlalchemy import desc, asc
from common.models.document import DocumentVersion, Document from common.models.document import DocumentVersion, Document
from common.utils.filtered_list_view import FilteredListView from eveai_app.views.list_views.filtered_list_view import FilteredListView
from common.utils.view_assistants import prepare_table_for_macro from common.utils.view_assistants import prepare_table_for_macro

View File

@@ -0,0 +1,179 @@
from datetime import datetime as dt, timezone as tz
from flask import request, render_template, session, current_app
from sqlalchemy import desc, asc, or_, and_
from sqlalchemy.orm import aliased
from common.models.document import Document, DocumentVersion
from eveai_app.views.list_views.filtered_list_view import FilteredListView
from common.utils.view_assistants import prepare_table_for_macro
class FullDocumentListView(FilteredListView):
allowed_filters = ['validity', 'file_type', 'processing', 'processing_error']
allowed_sorts = ['id', 'name', 'valid_from', 'valid_to', 'file_type', 'processing_started_at',
'processing_finished_at', 'processing_error']
def __init__(self, model, template, per_page=10):
super().__init__(model, template, per_page)
self.version_alias = None
def get_query(self):
catalog_id = session.get('catalog_id')
current_app.logger.debug(f"Catalog ID: {catalog_id}")
# Fix: Selecteer alleen de id kolom in de subquery
latest_version_subquery = (
DocumentVersion.query
.with_entities(DocumentVersion.id, DocumentVersion.doc_id, DocumentVersion.url,
DocumentVersion.bucket_name, DocumentVersion.object_name,
DocumentVersion.file_type, DocumentVersion.sub_file_type,
DocumentVersion.file_size, DocumentVersion.language,
DocumentVersion.user_context, DocumentVersion.system_context,
DocumentVersion.user_metadata, DocumentVersion.system_metadata,
DocumentVersion.catalog_properties, DocumentVersion.created_at,
DocumentVersion.created_by, DocumentVersion.updated_at,
DocumentVersion.updated_by, DocumentVersion.processing,
DocumentVersion.processing_started_at, DocumentVersion.processing_finished_at,
DocumentVersion.processing_error)
.filter(DocumentVersion.id == (
DocumentVersion.query
.with_entities(DocumentVersion.id) # Selecteer alleen de id kolom
.filter(DocumentVersion.doc_id == Document.id)
.order_by(DocumentVersion.id.desc())
.limit(1)
.scalar_subquery()
))
.subquery()
)
self.version_alias = aliased(DocumentVersion, latest_version_subquery)
return Document.query.filter_by(catalog_id=catalog_id).outerjoin(
self.version_alias, Document.id == self.version_alias.doc_id
)
def apply_filters(self, query):
filters = request.args.to_dict(flat=False)
# Document filters
if 'validity' in filters:
now = dt.now(tz.utc).date()
if 'valid' in filters['validity']:
query = query.filter(
and_(
or_(Document.valid_from.is_(None), Document.valid_from <= now),
or_(Document.valid_to.is_(None), Document.valid_to >= now)
)
)
# DocumentVersion filters - use the same alias from get_query
if filters.get('file_type') and self.version_alias is not None:
query = query.filter(self.version_alias.file_type == filters['file_type'][0])
if filters.get('processing') and self.version_alias is not None:
query = query.filter(self.version_alias.processing == (filters['processing'][0] == 'true'))
if filters.get('processing_error') and self.version_alias is not None:
if filters['processing_error'][0] == 'true':
query = query.filter(self.version_alias.processing_error.isnot(None))
elif filters['processing_error'][0] == 'false':
query = query.filter(self.version_alias.processing_error.is_(None))
# Controleer of start_date een waarde heeft voordat we proberen te parsen
if filters.get('start_date') and self.version_alias is not None and filters['start_date'][0].strip():
query = query.filter(
self.version_alias.processing_started_at >= dt.strptime(filters['start_date'][0], '%Y-%m-%d'))
# Controleer of end_date een waarde heeft voordat we proberen te parsen
if filters.get('end_date') and self.version_alias is not None and filters['end_date'][0].strip():
query = query.filter(
self.version_alias.processing_finished_at <= dt.strptime(filters['end_date'][0], '%Y-%m-%d'))
return query
def apply_sorting(self, query):
sort_by = request.args.get('sort_by', 'id')
sort_order = request.args.get('sort_order', 'asc')
document_columns = ['id', 'name', 'valid_from', 'valid_to']
version_columns = ['file_type', 'processing', 'processing_started_at', 'processing_finished_at',
'processing_error']
if sort_by in self.allowed_sorts:
if sort_by in document_columns:
column = getattr(Document, sort_by)
elif sort_by in version_columns and self.version_alias is not None:
column = getattr(self.version_alias, sort_by)
else:
column = Document.id
if sort_order == 'asc':
query = query.order_by(asc(column))
elif sort_order == 'desc':
query = query.order_by(desc(column))
return query
def get(self):
query = self.get_query()
query = self.apply_filters(query)
query = self.apply_sorting(query)
pagination = self.paginate(query)
# Haal de laatste versies op voor elke document
items_with_versions = []
for doc in pagination.items:
latest_version = DocumentVersion.query.filter_by(doc_id=doc.id).order_by(desc(DocumentVersion.id)).first()
items_with_versions.append((doc, latest_version))
def format_date(date):
if isinstance(date, dt):
return date.strftime('%Y-%m-%d')
elif isinstance(date, str):
return date
else:
return ''
# Maak rijen voor de tabel met document en versie informatie
rows = []
for doc, version in items_with_versions:
if version:
row = [
{'value': doc.id, 'class': '', 'type': 'text'},
{'value': doc.name, 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_from), 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_to), 'class': '', 'type': 'text'},
{'value': version.id, 'class': '', 'type': 'text'},
{'value': version.file_type, 'class': '', 'type': 'text'},
{'value': 'Ja' if version.processing else 'Nee', 'class': '', 'type': 'text'},
{'value': version.processing_error or '', 'class': '', 'type': 'text'}
]
else:
row = [
{'value': doc.id, 'class': '', 'type': 'text'},
{'value': doc.name, 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_from), 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_to), 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'}
]
rows.append(row)
context = {
'rows': rows,
'pagination': pagination,
'filters': request.args.to_dict(flat=False),
'sort_by': request.args.get('sort_by', 'id'),
'sort_order': request.args.get('sort_order', 'asc'),
'filter_options': self.get_filter_options()
}
return render_template(self.template, **context)
def get_filter_options(self):
return {
'validity': [('valid', 'Valid'), ('all', 'All')],
'file_type': [('pdf', 'PDF'), ('docx', 'DOCX')],
'processing': [('true', 'Processing'), ('false', 'Not Processing')],
'processing_error': [('true', 'With Errors'), ('false', 'Without Errors')]
}

View File

@@ -9,6 +9,7 @@ from pydantic import BaseModel, Field
from common.extensions import db, minio_client from common.extensions import db, minio_client
from common.models.interaction import Specialist, EveAIAsset from common.models.interaction import Specialist, EveAIAsset
from common.utils.minio_utils import MIB_CONVERTOR
from common.utils.eveai_exceptions import EveAISpecialistExecutionError from common.utils.eveai_exceptions import EveAISpecialistExecutionError
from common.utils.model_logging_utils import set_logging_information from common.utils.model_logging_utils import set_logging_information
from eveai_chat_workers.definitions.language_level.language_level_v1_0 import LANGUAGE_LEVEL from eveai_chat_workers.definitions.language_level.language_level_v1_0 import LANGUAGE_LEVEL
@@ -209,7 +210,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
# Stap 3: Storage metadata toevoegen # Stap 3: Storage metadata toevoegen
asset.bucket_name = bucket_name asset.bucket_name = bucket_name
asset.object_name = object_name asset.object_name = object_name
asset.file_size = file_size asset.file_size = file_size / MIB_CONVERTOR
asset.file_type = "json" asset.file_type = "json"
# Stap 4: Token usage toevoegen # Stap 4: Token usage toevoegen