- Created a new eveai_chat plugin to support the new dynamic possibilities of the Specialists. Currently only supports standard Rag retrievers (i.e. no extra arguments).

This commit is contained in:
Josako
2024-11-27 12:26:49 +01:00
parent 07d89d204f
commit 98cb4e4f2f
13 changed files with 462 additions and 405 deletions

View File

@@ -1,4 +1,4 @@
from flask import render_template, request, jsonify, redirect from flask import render_template, request, jsonify, redirect, current_app
from flask_login import current_user from flask_login import current_user
from common.utils.nginx_utils import prefixed_url_for from common.utils.nginx_utils import prefixed_url_for
@@ -6,24 +6,28 @@ from common.utils.nginx_utils import prefixed_url_for
def not_found_error(error): def not_found_error(error):
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login')) return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Not Found Error: {error}")
return render_template('error/404.html'), 404 return render_template('error/404.html'), 404
def internal_server_error(error): def internal_server_error(error):
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login')) return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Internal Server Error: {error}")
return render_template('error/500.html'), 500 return render_template('error/500.html'), 500
def not_authorised_error(error): def not_authorised_error(error):
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login')) return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Not Authorised Error: {error}")
return render_template('error/401.html') return render_template('error/401.html')
def access_forbidden(error): def access_forbidden(error):
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(prefixed_url_for('security.login')) return redirect(prefixed_url_for('security.login'))
current_app.logger.error(f"Access Forbidden: {error}")
return render_template('error/403.html') return render_template('error/403.html')
@@ -32,6 +36,7 @@ def key_error_handler(error):
if str(error) == "'tenant'": if str(error) == "'tenant'":
return redirect(prefixed_url_for('security.login')) return redirect(prefixed_url_for('security.login'))
# For other KeyErrors, you might want to log the error and return a generic error page # For other KeyErrors, you might want to log the error and return a generic error page
current_app.logger.error(f"Key Error: {error}")
return render_template('error/generic.html', error_message="An unexpected error occurred"), 500 return render_template('error/generic.html', error_message="An unexpected error occurred"), 500

View File

@@ -10,6 +10,17 @@
<div class="container"> <div class="container">
<form method="POST" action="{{ url_for('document_bp.handle_library_selection') }}"> <form method="POST" action="{{ url_for('document_bp.handle_library_selection') }}">
<div class="form-group mt-3"> <div class="form-group mt-3">
<h2>Create Default RAG Library</h2>
<p>This function will create a default library setup for RAG purposes. More specifically, it will create:</p>
<ul>
<li>A default RAG Catalog</li>
<li>A Default HTML Processor</li>
<li>A default RAG Retriever</li>
<li>A default RAG Specialist</li>
</ul>
<p>This enables a quick start-up for standard Ask Eve AI functionality. All elements can be changed later on an individual basis.</p>
<button type="submit" name="action" value="create_default_rag_library" class="btn btn-danger">Create Default RAG Library</button>
<h2>Re-Embed Latest Versions</h2> <h2>Re-Embed Latest Versions</h2>
<p>This functionality will re-apply embeddings on the latest versions of all documents in the library. <p>This functionality will re-apply embeddings on the latest versions of all documents in the library.
This is useful only while tuning the embedding parameters, or when changing embedding algorithms. This is useful only while tuning the embedding parameters, or when changing embedding algorithms.
@@ -17,6 +28,7 @@
use it with caution! use it with caution!
</p> </p>
<button type="submit" name="action" value="re_embed_latest_versions" class="btn btn-danger">Re-embed Latest Versions (expensive)</button> <button type="submit" name="action" value="re_embed_latest_versions" class="btn btn-danger">Re-embed Latest Versions (expensive)</button>
<h2>Refresh all documents</h2> <h2>Refresh all documents</h2>
<p>This operation will create new versions of all documents in the library with a URL. Documents that were uploaded directly, <p>This operation will create new versions of all documents in the library with a URL. Documents that were uploaded directly,
cannot be automatically refreshed. This is an expensive operation, and impacts the performance of the system in future use. cannot be automatically refreshed. This is an expensive operation, and impacts the performance of the system in future use.

View File

@@ -9,13 +9,10 @@
{% block content %} {% block content %}
<form action="{{ url_for('entitlements_bp.handle_license_selection') }}" method="POST"> <form action="{{ url_for('entitlements_bp.handle_license_selection') }}" method="POST">
{{ render_selectable_table(headers=["License ID", "Name", "Start Date", "End Date", "Active"], rows=rows, selectable=True, id="licensesTable") }} {{ render_selectable_table(headers=["License ID", "Name", "Start Date", "End Date", "Active"], rows=rows, selectable=True, id="licensesTable") }}
<!-- <div class="form-group mt-3">--> <div class="form-group mt-3">
<!-- <button type="submit" name="action" value="edit_user" class="btn btn-primary">Edit Selected User</button>--> <button type="submit" name="action" value="edit_license" class="btn btn-primary">Edit License</button>
<!-- <button type="submit" name="action" value="resend_confirmation_email" class="btn btn-secondary">Resend Confirmation Email</button>--> <!-- Additional buttons can be added here for other actions -->
<!-- <button type="submit" name="action" value="send_password_reset_email" class="btn btn-secondary">Send Password Reset Email</button>--> </div>
<!-- <button type="submit" name="action" value="reset_uniquifier" class="btn btn-secondary">Reset Uniquifier</button>-->
<!-- &lt;!&ndash; Additional buttons can be added here for other actions &ndash;&gt;-->
<!-- </div>-->
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros.html" import render_field %}
{% block title %}Session Overview{% endblock %} {% block title %}Session Overview{% endblock %}
{% block content_title %}Session Overview{% endblock %} {% block content_title %}Session Overview{% endblock %}
@@ -8,7 +6,7 @@
{% block content %} {% block content %}
<div class="container mt-5"> <div class="container mt-5">
<h2>Chat Session Details</h2> <h4>Chat Session Details</h4>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h5>Session Information</h5> <h5>Session Information</h5>
@@ -21,44 +19,73 @@
</div> </div>
</div> </div>
<h3>Interactions</h3> <h5>Interactions</h5>
<div class="accordion" id="interactionsAccordion"> <div class="accordion" id="interactionsAccordion">
{% for interaction in interactions %} {% for interaction, id, question_at, specialist_arguments, specialist_results, specialist_name, specialist_type in interactions %}
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="heading{{ loop.index }}"> <p class="accordion-header" id="heading{{ loop.index }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapse{{ loop.index }}" aria-expanded="false" data-bs-target="#collapse{{ loop.index }}" aria-expanded="false"
aria-controls="collapse{{ loop.index }}"> aria-controls="collapse{{ loop.index }}">
<div class="d-flex justify-content-between align-items-center w-100"> <div class="interaction-header">
<span class="interaction-question">{{ interaction.question | truncate(50) }}</span> <div class="interaction-metadata">
<span class="interaction-icons"> <div class="interaction-time text-muted">
<i class="material-icons algorithm-icon {{ interaction.algorithm_used | lower }}">fingerprint</i> {{ question_at | to_local_time(chat_session.timezone) }}
<i class="material-icons thumb-icon {% if interaction.appreciation == 100 %}filled{% else %}outlined{% endif %}">thumb_up</i> </div>
<i class="material-icons thumb-icon {% if interaction.appreciation == 0 %}filled{% else %}outlined{% endif %}">thumb_down</i> <div class="specialist-info">
</span> <span class="badge bg-primary">{{ specialist_name if specialist_name else 'No Specialist' }}</span>
<span class="badge bg-secondary">{{ specialist_type if specialist_type else '' }}</span>
</div>
</div>
<div class="interaction-question">
{{ specialist_results.detailed_query if specialist_results and specialist_results.detailed_query else specialist_arguments.query }}
</div>
</div> </div>
</button> </button>
</h2> </p>
<div id="collapse{{ loop.index }}" class="accordion-collapse collapse" aria-labelledby="heading{{ loop.index }}" <div id="collapse{{ loop.index }}" class="accordion-collapse collapse" aria-labelledby="heading{{ loop.index }}"
data-bs-parent="#interactionsAccordion"> data-bs-parent="#interactionsAccordion">
<div class="accordion-body"> <div class="accordion-body">
<h6>Detailed Question:</h6> <!-- Arguments Section -->
<p>{{ interaction.detailed_question }}</p> {% if specialist_arguments %}
<h6>Answer:</h6> <div class="mb-4">
<div class="markdown-content">{{ interaction.answer | safe }}</div> <h6 class="mb-3">Specialist Arguments:</h6>
{% if embeddings_dict.get(interaction.id) %} <div class="code-wrapper">
<pre><code class="language-json" style="width: 100%;">{{ specialist_arguments | tojson(indent=2) }}</code></pre>
</div>
</div>
{% endif %}
<!-- Results Section -->
{% if specialist_results %}
<div class="mb-4">
<h6 class="mb-3">Specialist Results:</h6>
<div class="code-wrapper">
<pre><code class="language-json" style="width: 100%;">{{ specialist_results | tojson(indent=2) }}</code></pre>
</div>
</div>
{% endif %}
<!-- Related Documents Section -->
{% if embeddings_dict.get(id) %}
<div class="mt-4">
<h6>Related Documents:</h6> <h6>Related Documents:</h6>
<ul> <ul class="list-group">
{% for embedding in embeddings_dict[interaction.id] %} {% for embedding in embeddings_dict[id] %}
<li> <li class="list-group-item">
{% if embedding.url %} {% if embedding.url %}
<a href="{{ embedding.url }}" target="_blank">{{ embedding.url }}</a> <a href="{{ embedding.url }}" target="_blank" class="text-decoration-none">
<i class="material-icons align-middle me-2">link</i>
{{ embedding.url }}
</a>
{% else %} {% else %}
<i class="material-icons align-middle me-2">description</i>
{{ embedding.object_name }} {{ embedding.object_name }}
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -68,14 +95,166 @@
</div> </div>
{% endblock %} {% endblock %}
{% block styles %}
{{ super() }}
<style>
.interaction-header {
font-size: 0.9rem;
display: flex;
flex-direction: column;
width: 100%;
padding: 0.5rem 0;
}
.interaction-metadata {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
}
.interaction-time {
font-size: 0.9rem;
}
.specialist-info {
display: flex;
gap: 0.5rem;
align-items: center;
}
.interaction-question {
font-size: 0.9rem;
font-weight: bold;
line-height: 1.4;
}
.badge {
font-size: 0.9rem;
padding: 0.35em 0.65em;
white-space: nowrap;
}
.accordion-button {
padding: 0.5rem 1rem;
}
.accordion-button::after {
margin-left: 1rem;
}
.json-display {
background-color: #f8f9fa;
border-radius: 4px;
padding: 15px;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: monospace;
font-size: 0.85rem;
line-height: 1.5;
max-width: 100%;
overflow-x: auto;
}
.list-group-item {
font-size: 0.9rem;
}
.material-icons {
font-size: 1.1rem;
}
pre {
margin: 0;
padding: 0;
white-space: pre-wrap !important; /* Force wrapping */
word-wrap: break-word !important; /* Break long words if necessary */
max-width: 100%; /* Ensure container doesn't overflow */
}
pre, code {
margin: 0;
padding: 0;
white-space: pre-wrap !important; /* Force wrapping */
word-wrap: break-word !important; /* Break long words if necessary */
max-width: 100%; /* Ensure container doesn't overflow */
}
pre code {
padding: 1rem !important;
border-radius: 4px;
font-size: 0.75rem;
line-height: 1.5;
white-space: pre-wrap !important; /* Force wrapping in code block */
}
.code-wrapper {
position: relative;
width: 100%;
}
/* Override all possible highlight.js white-space settings */
.code-wrapper pre,
.code-wrapper pre code,
.code-wrapper pre code.hljs,
.code-wrapper .hljs {
white-space: pre-wrap !important;
overflow-wrap: break-word !important;
word-wrap: break-word !important;
word-break: break-word !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
.code-wrapper pre {
margin: 0;
background: #f8f9fa;
border-radius: 4px;
}
.code-wrapper pre code {
padding: 1rem !important;
font-family: monospace;
font-size: 0.9rem;
line-height: 1.5;
display: block;
}
/* Override highlight.js default nowrap behavior */
.hljs {
background: #f8f9fa !important;
white-space: pre-wrap !important;
word-wrap: break-word !important;
}
/* Color theme */
.hljs-string {
color: #0a3069 !important;
}
.hljs-attr {
color: #953800 !important;
}
.hljs-number {
color: #116329 !important;
}
.hljs-boolean {
color: #0550ae !important;
}
</style>
{% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> {{ super() }}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var markdownElements = document.querySelectorAll('.markdown-content'); // Initialize syntax highlighting
markdownElements.forEach(function(el) { document.querySelectorAll('pre code').forEach((block) => {
el.innerHTML = marked.parse(el.textContent); hljs.highlightElement(block);
});
}); });
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,10 +1,4 @@
{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %} {% macro render_field_content(field, disabled=False, class='') %}
<!-- Debug info -->
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
{% set disabled = field.name in disabled_fields %}
{% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}
{% if field.name not in exclude_fields %}
{% if field.type == 'BooleanField' %} {% if field.type == 'BooleanField' %}
<div class="form-group"> <div class="form-group">
<div class="form-check form-switch"> <div class="form-check form-switch">
@@ -79,83 +73,24 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% endmacro %}
{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %}
<!-- Debug info -->
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
{% set disabled = field.name in disabled_fields %}
{% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}
{% if field.name not in exclude_fields %}
{{ render_field_content(field, disabled, class) }}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro render_included_field(field, disabled_fields=[], include_fields=[]) %} {% macro render_included_field(field, disabled_fields=[], include_fields=[], class='') %}
{% set disabled = field.name in disabled_fields %} {% set disabled = field.name in disabled_fields %}
{% if field.name in include_fields %} {% if field.name in include_fields %}
{% if field.type == 'BooleanField' %} {{ render_field_content(field, disabled, class) }}
<div class="form-check">
{{ field(class="form-check-input", type="checkbox", id="flexSwitchCheckDefault") }}
{% if field.description %}
{{ field.label(class="form-check-label",
for="flexSwitchCheckDefault",
disabled=disabled,
**{'data-bs-toggle': 'tooltip',
'data-bs-placement': 'right',
'title': field.description}) }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
{% else %}
{{ field.label(class="form-check-label", for="flexSwitchCheckDefault", disabled=disabled) }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
{% endif %}
</div>
{% else %}
<div class="form-group">
{% if field.description %}
<div class="field-label-wrapper">
{{ field.label(class="form-label",
**{'data-bs-toggle': 'tooltip',
'data-bs-placement': 'right',
'title': field.description}) }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
</div>
{% else %}
<div class="field-label-wrapper">
{{ field.label(class="form-label") }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
</div>
{% endif %}
{% if field.type == 'TextAreaField' and 'json-editor' in field.render_kw.get('class', '') %}
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
{{ field(class="form-control d-none", disabled=disabled) }}
{% elif field.type == 'SelectField' %}
{{ field(class="form-control form-select", disabled=disabled) }}
{% else %}
{{ field(class="form-control", disabled=disabled) }}
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

View File

@@ -109,7 +109,7 @@
{'name': 'License Tier Registration', 'url': '/entitlements/license_tier', 'roles': ['Super User']}, {'name': 'License Tier Registration', 'url': '/entitlements/license_tier', 'roles': ['Super User']},
{'name': 'All License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User']}, {'name': 'All License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User']},
{'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']}, {'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']},
{'name': 'All Licenses', 'url': 'entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin']}, {'name': 'All Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']}, {'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']},
]) }} ]) }}
{% endif %} {% endif %}

View File

@@ -9,65 +9,12 @@
{% block content %} {% block content %}
<form method="post"> <form method="post">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<!-- Main Tenant Information --> {% set disabled_fields = [] %}
{% set main_fields = ['name', 'website', 'default_language', 'allowed_languages', 'timezone','rag_context', 'type'] %} {% set exclude_fields = [] %}
{% for field in form %} {% for field in form %}
{{ render_included_field(field, disabled_fields=[], include_fields=main_fields) }} {{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %} {% endfor %}
<!-- Nav Tabs -->
<div class="row mt-5">
<div class="col-lg-12">
<div class="nav-wrapper position-relative end-0">
<ul class="nav nav-pills nav-fill p-1" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#model-info-tab" role="tab" aria-controls="model-info" aria-selected="true">
Model Information
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#license-info-tab" role="tab" aria-controls="license-info" aria-selected="false">
License Information
</a>
</li>
</ul>
</div>
<div class="tab-content tab-space">
<!-- Model Information Tab -->
<div class="tab-pane fade show active" id="model-info-tab" role="tabpanel">
{% set model_fields = ['embedding_model', 'llm_model'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=[], include_fields=model_fields) }}
{% endfor %}
</div>
<!-- License Information Tab -->
<div class="tab-pane fade" id="license-info-tab" role="tabpanel">
{% set license_fields = ['currency', 'usage_email', ] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=[], include_fields=license_fields) }}
{% endfor %}
<!-- Register API Key Button -->
<div class="form-group">
<button type="button" class="btn btn-primary" onclick="generateNewChatApiKey()">Register Chat API Key</button>
<button type="button" class="btn btn-primary" onclick="generateNewApiKey()">Register API Key</button>
</div>
<!-- API Key Display Field -->
<div id="chat-api-key-field" style="display:none;">
<label for="chat-api-key">Chat API Key:</label>
<input type="text" id="chat-api-key" class="form-control" readonly>
<button type="button" id="copy-chat-button" class="btn btn-primary">Copy to Clipboard</button>
<p id="copy-chat-message" style="display:none;color:green;">Chat API key copied to clipboard</p>
</div>
<div id="api-key-field" style="display:none;">
<label for="api-key">API Key:</label>
<input type="text" id="api-key" class="form-control" readonly>
<button type="button" id="copy-api-button" class="btn btn-primary">Copy to Clipboard</button>
<p id="copy-message" style="display:none;color:green;">API key copied to clipboard</p>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save Tenant</button> <button type="submit" class="btn btn-primary">Save Tenant</button>
</form> </form>
{% endblock %} {% endblock %}
@@ -78,88 +25,6 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script>
// Function to generate a new Chat API Key
function generateNewChatApiKey() {
generateApiKey('/admin/user/generate_chat_api_key', '#chat-api-key', '#chat-api-key-field');
}
// Function to generate a new general API Key
function generateNewApiKey() {
generateApiKey('/admin/user/generate_api_api_key', '#api-key', '#api-key-field');
}
// Reusable function to handle API key generation
function generateApiKey(url, inputSelector, fieldSelector) {
$.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
success: function(response) {
$(inputSelector).val(response.api_key);
$(fieldSelector).show();
},
error: function(error) {
alert('Error generating new API key: ' + error.responseText);
}
});
}
// Function to copy text to clipboard
function copyToClipboard(selector, messageSelector) {
const element = document.querySelector(selector);
if (element) {
const text = element.value;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
showCopyMessage(messageSelector);
}).catch(function(error) {
alert('Failed to copy text: ' + error);
});
} else {
fallbackCopyToClipboard(text, messageSelector);
}
} else {
console.error('Element not found for selector:', selector);
}
}
// Fallback method for copying text to clipboard
function fallbackCopyToClipboard(text, messageSelector) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopyMessage(messageSelector);
} catch (err) {
alert('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
}
// Function to show copy confirmation message
function showCopyMessage(messageSelector) {
const message = document.querySelector(messageSelector);
if (message) {
message.style.display = 'block';
setTimeout(function() {
message.style.display = 'none';
}, 2000);
}
}
// Event listeners for copy buttons
document.getElementById('copy-chat-button').addEventListener('click', function() {
copyToClipboard('#chat-api-key', '#copy-chat-message');
});
document.getElementById('copy-api-button').addEventListener('click', function() {
copyToClipboard('#api-key', '#copy-message');
});
</script>
<script> <script>
// JavaScript to detect user's timezone // JavaScript to detect user's timezone
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {

View File

@@ -13,6 +13,7 @@ import json
from common.models.document import Document, DocumentVersion, Catalog, Retriever, Processor from common.models.document import Document, DocumentVersion, Catalog, Retriever, Processor
from common.extensions import db from common.extensions import db
from common.models.interaction import Specialist, SpecialistRetriever
from common.utils.document_utils import validate_file_type, create_document_stack, start_embedding_task, process_url, \ from common.utils.document_utils import validate_file_type, create_document_stack, start_embedding_task, process_url, \
edit_document, \ edit_document, \
edit_document_version, refresh_document edit_document_version, refresh_document
@@ -663,6 +664,8 @@ def handle_library_selection():
action = request.form['action'] action = request.form['action']
match action: match action:
case 'create_default_rag_library':
create_default_rag_library()
case 're_embed_latest_versions': case 're_embed_latest_versions':
re_embed_latest_versions() re_embed_latest_versions()
case 'refresh_all_documents': case 'refresh_all_documents':
@@ -671,6 +674,86 @@ def handle_library_selection():
return redirect(prefixed_url_for('document_bp.library_operations')) return redirect(prefixed_url_for('document_bp.library_operations'))
def create_default_rag_library():
# Check if no catalog exists. If non exists, no processors, retrievers or specialist can exists
catalogs = Catalog.query.all()
if catalogs:
flash("Default RAG Library can only be created if no catalogs are defined!", 'danger')
return redirect(prefixed_url_for('document_bp.library_operations'))
timestamp = dt.now(tz=tz.utc)
try:
cat = Catalog(
name='Default RAG Catalog',
description='Default RAG Catalog',
type="STANDARD_CATALOG",
min_chunk_size=2000,
max_chunk_size=3000,
)
set_logging_information(cat, timestamp)
db.session.add(cat)
db.session.commit()
proc = Processor(
name='Default HTML Processor',
description='Default HTML Processor',
catalog_id=cat.id,
type="HTML Processor",
configuration={
"html_tags": "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td",
"html_end_tags": "p, li, table",
"html_excluded_classes": "",
"html_excluded_elements": "header, footer, nav, script",
"html_included_elements": "article, main"
}
)
set_logging_information(proc, timestamp)
retr = Retriever(
name='Default RAG Retriever',
description='Default RAG Retriever',
catalog_id=cat.id,
type="STANDARD_RAG",
configuration={
"es_k": "8",
"es_similarity_threshold": 0.3
}
)
set_logging_information(retr, timestamp)
db.session.add(proc)
db.session.add(retr)
db.session.commit()
spec = Specialist(
name='Default RAG Specialist',
description='Default RAG Specialist',
type='STANDARD_RAG',
configuration={"temperature": "0.3", "specialist_context": "To be specified"}
)
set_logging_information(spec, timestamp)
db.session.add(spec)
db.session.commit()
spec_retr = SpecialistRetriever(
specialist_id=spec.id,
retriever_id=retr.id,
)
db.session.add(spec_retr)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to create Default RAG Library. Error: {e}', 'danger')
current_app.logger.error(f'Failed to create Default RAG Library'
f'for tenant {session['tenant']['id']}. Error: {str(e)}')
return redirect(prefixed_url_for('document_bp.library_operations'))
@document_bp.route('/document_versions_list', methods=['GET']) @document_bp.route('/document_versions_list', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def document_versions_list(): def document_versions_list():

View File

@@ -48,7 +48,7 @@ class LicenseForm(FlaskForm):
start_date = DateField('Start Date', id='form-control datepicker', validators=[DataRequired()]) start_date = DateField('Start Date', id='form-control datepicker', validators=[DataRequired()])
end_date = DateField('End Date', id='form-control datepicker', validators=[DataRequired()]) end_date = DateField('End Date', id='form-control datepicker', validators=[DataRequired()])
currency = StringField('Currency', validators=[Optional(), Length(max=20)]) currency = StringField('Currency', validators=[Optional(), Length(max=20)])
yearly_payment = BooleanField('Yearly Payment', validators=[DataRequired()], default=False) yearly_payment = BooleanField('Yearly Payment', default=False)
basic_fee = FloatField('Basic Fee', validators=[InputRequired(), NumberRange(min=0)]) basic_fee = FloatField('Basic Fee', validators=[InputRequired(), NumberRange(min=0)])
max_storage_mb = IntegerField('Max Storage (MiB)', validators=[DataRequired(), NumberRange(min=1)]) max_storage_mb = IntegerField('Max Storage (MiB)', validators=[DataRequired(), NumberRange(min=1)])
additional_storage_price = FloatField('Additional Storage Token Fee', additional_storage_price = FloatField('Additional Storage Token Fee',

View File

@@ -204,12 +204,11 @@ def edit_license(license_id):
flash('License updated successfully.', 'success') flash('License updated successfully.', 'success')
return redirect( return redirect(
prefixed_url_for('entitlements_bp.edit_license', license_tier_id=license_id)) prefixed_url_for('entitlements_bp.edit_license', license_id=license_id))
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
return render_template('entitlements/license.html', form=form, license_tier_id=license_tier.id, return render_template('entitlements/license.html', form=form, ext_disabled_fields=disabled_fields)
ext_disabled_fields=disabled_fields)
@entitlements_bp.route('/view_usages') @entitlements_bp.route('/view_usages')
@@ -258,13 +257,14 @@ def view_licenses():
# Query licenses for the tenant, with ordering and active status # Query licenses for the tenant, with ordering and active status
query = ( query = (
License.query.filter_by(tenant_id=tenant_id) License.query
.join(LicenseTier) # Join with LicenseTier
.filter(License.tenant_id == tenant_id)
.add_columns( .add_columns(
License.id, License.id,
License.start_date, License.start_date,
License.end_date, License.end_date,
License.license_tier, LicenseTier.name.label('license_tier_name'), # Access name through LicenseTier
License.license_tier.name.label('license_tier_name'),
((License.start_date <= current_date) & ((License.start_date <= current_date) &
(or_(License.end_date.is_(None), License.end_date >= current_date))).label('active') (or_(License.end_date.is_(None), License.end_date >= current_date))).label('active')
) )
@@ -276,7 +276,7 @@ def view_licenses():
# prepare table data # prepare table data
rows = prepare_table_for_macro(lics, [('id', ''), ('license_tier_name', ''), ('start_date', ''), ('end_date', ''), rows = prepare_table_for_macro(lics, [('id', ''), ('license_tier_name', ''), ('start_date', ''), ('end_date', ''),
('active', ''),]) ('active', '')])
# Render the licenses in a template # Render the licenses in a template
return render_template('entitlements/view_licenses.html', rows=rows, pagination=pagination) return render_template('entitlements/view_licenses.html', rows=rows, pagination=pagination)
@@ -287,8 +287,10 @@ def view_licenses():
def handle_license_selection(): def handle_license_selection():
license_identification = request.form['selected_row'] license_identification = request.form['selected_row']
license_id = ast.literal_eval(license_identification).get('value') license_id = ast.literal_eval(license_identification).get('value')
the_license = LicenseUsage.query.get_or_404(license_id) the_license = License.query.get_or_404(license_id)
action = request.form['action'] action = request.form['action']
pass # Currently, no actions are defined match action:
case 'edit_license':
return redirect(prefixed_url_for('entitlements_bp.edit_license', license_id=license_id))

View File

@@ -72,11 +72,21 @@ def handle_chat_session_selection():
@interaction_bp.route('/view_chat_session/<int:chat_session_id>', methods=['GET']) @interaction_bp.route('/view_chat_session/<int:chat_session_id>', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def view_chat_session(chat_session_id): def view_chat_session(chat_session_id):
# Get chat session with user info
chat_session = ChatSession.query.get_or_404(chat_session_id) chat_session = ChatSession.query.get_or_404(chat_session_id)
# Get interactions with specialist info
interactions = (Interaction.query interactions = (Interaction.query
.filter_by(chat_session_id=chat_session.id) .filter_by(chat_session_id=chat_session.id)
.order_by(Interaction.question_at) .join(Specialist, Interaction.specialist_id == Specialist.id, isouter=True)
.all()) .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())
# Fetch all related embeddings for the interactions in this session # Fetch all related embeddings for the interactions in this session
embedding_query = (db.session.query(InteractionEmbedding.interaction_id, embedding_query = (db.session.query(InteractionEmbedding.interaction_id,
@@ -84,7 +94,7 @@ def view_chat_session(chat_session_id):
DocumentVersion.object_name) DocumentVersion.object_name)
.join(Embedding, InteractionEmbedding.embedding_id == Embedding.id) .join(Embedding, InteractionEmbedding.embedding_id == Embedding.id)
.join(DocumentVersion, Embedding.doc_vers_id == DocumentVersion.id) .join(DocumentVersion, Embedding.doc_vers_id == DocumentVersion.id)
.filter(InteractionEmbedding.interaction_id.in_([i.id for i in interactions]))) .filter(InteractionEmbedding.interaction_id.in_([i.id for i, *_ in interactions])))
# Create a dictionary to store embeddings for each interaction # Create a dictionary to store embeddings for each interaction
embeddings_dict = {} embeddings_dict = {}

View File

@@ -68,7 +68,7 @@ def tenant():
current_app.logger.info(f"Creating MinIO bucket for tenant {new_tenant.id}") current_app.logger.info(f"Creating MinIO bucket for tenant {new_tenant.id}")
minio_client.create_tenant_bucket(new_tenant.id) minio_client.create_tenant_bucket(new_tenant.id)
return redirect(prefixed_url_for('basic_bp.index')) return redirect(prefixed_url_for('user_bp.select_tenant'))
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
@@ -378,66 +378,6 @@ def edit_tenant_domain(tenant_domain_id):
return render_template('user/edit_tenant_domain.html', form=form, tenant_domain_id=tenant_domain_id) return render_template('user/edit_tenant_domain.html', form=form, tenant_domain_id=tenant_domain_id)
@user_bp.route('/check_chat_api_key', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def check_chat_api_key():
tenant_id = session['tenant']['id']
tenant = Tenant.query.get_or_404(tenant_id)
if tenant.encrypted_chat_api_key:
return jsonify({'api_key_exists': True})
return jsonify({'api_key_exists': False})
@user_bp.route('/generate_chat_api_key', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def generate_chat_api_key():
tenant = Tenant.query.get_or_404(session['tenant']['id'])
new_api_key = generate_api_key(prefix="EveAI-CHAT")
tenant.encrypted_chat_api_key = simple_encryption.encrypt_api_key(new_api_key)
update_logging_information(tenant, dt.now(tz.utc))
try:
db.session.add(tenant)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f'Unable to store chat api key for tenant {tenant.id}. Error: {str(e)}')
return jsonify({'api_key': new_api_key}), 200
@user_bp.route('/check_api_api_key', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def check_api_api_key():
tenant_id = session['tenant']['id']
tenant = Tenant.query.get_or_404(tenant_id)
if tenant.encrypted_api_key:
return jsonify({'api_key_exists': True})
return jsonify({'api_key_exists': False})
@user_bp.route('/generate_api_api_key', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def generate_api_api_key():
tenant = Tenant.query.get_or_404(session['tenant']['id'])
new_api_key = generate_api_key(prefix="EveAI-API")
tenant.encrypted_api_key = simple_encryption.encrypt_api_key(new_api_key)
update_logging_information(tenant, dt.now(tz.utc))
try:
db.session.add(tenant)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f'Unable to store api key for tenant {tenant.id}. Error: {str(e)}')
return jsonify({'api_key': new_api_key}), 200
@user_bp.route('/tenant_overview', methods=['GET']) @user_bp.route('/tenant_overview', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def tenant_overview(): def tenant_overview():

View File

@@ -1,4 +1,5 @@
# retrievers/standard_rag.py # retrievers/standard_rag.py
import json
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
from typing import Dict, Any, List from typing import Dict, Any, List
from sqlalchemy import func, or_, desc from sqlalchemy import func, or_, desc
@@ -35,6 +36,32 @@ class StandardRAGRetriever(BaseRetriever):
def type(self) -> str: def type(self) -> str:
return "STANDARD_RAG" return "STANDARD_RAG"
def _parse_metadata(self, metadata: Any) -> Dict[str, Any]:
"""
Parse metadata ensuring it's a dictionary
Args:
metadata: Input metadata which could be string, dict, or None
Returns:
Dict[str, Any]: Parsed metadata as dictionary
"""
if metadata is None:
return {}
if isinstance(metadata, dict):
return metadata
if isinstance(metadata, str):
try:
return json.loads(metadata)
except json.JSONDecodeError:
current_app.logger.warning(f"Failed to parse metadata JSON string: {metadata}")
return {}
current_app.logger.warning(f"Unexpected metadata type: {type(metadata)}")
return {}
def retrieve(self, arguments: RetrieverArguments) -> List[RetrieverResult]: def retrieve(self, arguments: RetrieverArguments) -> List[RetrieverResult]:
""" """
Retrieve documents based on query Retrieve documents based on query
@@ -92,6 +119,8 @@ class StandardRAGRetriever(BaseRetriever):
# Transform results into standard format # Transform results into standard format
processed_results = [] processed_results = []
for doc, similarity in results: for doc, similarity in results:
# Parse user_metadata to ensure it's a dictionary
user_metadata = self._parse_metadata(doc.document_version.user_metadata)
processed_results.append( processed_results.append(
RetrieverResult( RetrieverResult(
id=doc.id, id=doc.id,
@@ -101,7 +130,7 @@ class StandardRAGRetriever(BaseRetriever):
document_id=doc.document_version.doc_id, document_id=doc.document_version.doc_id,
version_id=doc.document_version.id, version_id=doc.document_version.id,
document_name=doc.document_version.document.name, document_name=doc.document_version.document.name,
user_metadata=doc.document_version.user_metadata or {}, user_metadata=user_metadata,
) )
) )
) )