3 Commits

Author SHA1 Message Date
Josako
443104dfc7 - Start Dev of User Action Agent. TBC 2025-11-20 11:13:41 +01:00
Josako
f2604db5a9 - Problem adding documents 2025-11-18 11:14:49 +01:00
Josako
78043ab3ef - Problem editing user model (Boolean values not set) 2025-11-17 10:43:21 +01:00
12 changed files with 156 additions and 58 deletions

View File

@@ -24,6 +24,7 @@ class Database:
""" """
schema = session.info.get("tenant_schema") schema = session.info.get("tenant_schema")
if schema: if schema:
current_app.logger.debug(f"DBCTX tx_begin schema={schema}")
try: try:
connection.exec_driver_sql(f'SET LOCAL search_path TO "{schema}", public') connection.exec_driver_sql(f'SET LOCAL search_path TO "{schema}", public')
# Optional visibility/logging for debugging # Optional visibility/logging for debugging

View File

@@ -35,7 +35,8 @@ def is_valid_tenant(tenant_id):
if tenant_id == 1: # The 'root' tenant, is always valid if tenant_id == 1: # The 'root' tenant, is always valid
return True return True
tenant = Tenant.query.get(tenant_id) tenant = Tenant.query.get(tenant_id)
Database(tenant).switch_schema() # Use the tenant_id (schema name), not the Tenant object, to switch schema
Database(tenant_id).switch_schema()
if tenant is None: if tenant is None:
raise EveAITenantNotFound() raise EveAITenantNotFound()
elif tenant.type == 'Inactive': elif tenant.type == 'Inactive':

View File

@@ -0,0 +1,15 @@
version: "1.0.0"
content: |
Classify the prompt you receive from an end user, according to the following information:
{user_action_classes}
Use the CLASS DESCRIPTION to identify the CLASS of the question asked. Return the value of CLASS. If the prompt doesn't correspond to any CLASS DESCRIPTION, return NONE. No layout is required.
llm_model: "mistral.mistral-small-latest"
temperature: 0.7
metadata:
author: "Josako"
date_added: "2025-11-14"
description: "Assistant to classify user intent"
changes: "Initial version"

View File

@@ -9,6 +9,12 @@
{% block content %} {% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{# Debug: render CSRF veld expliciet om aanwezigheid in de DOM te garanderen #}
{% if form.csrf_token %}{{ form.csrf_token }}{% endif %}
<script>
// Client-side debug: bevestig dat het CSRF veld in de DOM staat
console.debug('[add_document] CSRF present in DOM?', !!document.querySelector('input[name="csrf_token"]'));
</script>
{% set disabled_fields = [] %} {% set disabled_fields = [] %}
{% set exclude_fields = [] %} {% set exclude_fields = [] %}
{% for field in form.get_static_fields() %} {% for field in form.get_static_fields() %}

View File

@@ -11,18 +11,18 @@
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% set disabled_fields = [] %} {% set disabled_fields = [] %}
{% set exclude_fields = [] %} {% set exclude_fields = [] %}
{% for field in form.get_static_fields() %} {% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }} {{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %} {% endfor %}
<!-- Render Dynamic Fields --> <!-- Render Dynamic Fields -->
{% for collection_name, fields in form.get_dynamic_fields().items() %} {# {% for collection_name, fields in form.get_dynamic_fields().items() %}#}
{% if fields|length > 0 %} {# {% if fields|length > 0 %}#}
<h4 class="mt-4">{{ collection_name }}</h4> {# <h4 class="mt-4">{{ collection_name }}</h4>#}
{% endif %} {# {% endif %}#}
{% for field in fields %} {# {% for field in fields %}#}
{{ render_field(field, disabled_fields, exclude_fields) }} {# {{ render_field(field, disabled_fields, exclude_fields) }}#}
{% endfor %} {# {% endfor %}#}
{% endfor %} {# {% endfor %}#}
<button type="submit" class="btn btn-primary">Register Tenant Make</button> <button type="submit" class="btn btn-primary">Register Tenant Make</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -6,6 +6,7 @@ from flask_security import roles_accepted, current_user
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from werkzeug.datastructures import CombinedMultiDict
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
import requests import requests
from requests.exceptions import SSLError, HTTPError from requests.exceptions import SSLError, HTTPError
@@ -354,7 +355,17 @@ def handle_retriever_selection():
@document_bp.route('/add_document', methods=['GET', 'POST']) @document_bp.route('/add_document', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def add_document(): def add_document():
form = AddDocumentForm(request.form) # Log vroege request-info om uploadproblemen te diagnosticeren
try:
current_app.logger.debug(
f"[add_document] method={request.method}, content_type={request.content_type}, "
f"files_keys={list(request.files.keys())}"
)
except Exception:
pass
# Bind expliciet zowel form- als file-data aan de form (belangrijk voor FileField & CSRF)
form = AddDocumentForm(CombinedMultiDict([request.form, request.files]))
catalog_id = session.get('catalog_id', None) catalog_id = session.get('catalog_id', None)
if catalog_id is None: if catalog_id is None:
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning') flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
@@ -364,6 +375,38 @@ def add_document():
if catalog.configuration and len(catalog.configuration) > 0: if catalog.configuration and len(catalog.configuration) > 0:
form.add_dynamic_fields("tagging_fields", catalog.configuration) form.add_dynamic_fields("tagging_fields", catalog.configuration)
current_app.logger.debug("In Add Document")
# Extra debug logging om CSRF/payload te controleren
try:
current_app.logger.debug(
f"[add_document] request.form keys: {list(request.form.keys())}"
)
current_app.logger.debug(
f"[add_document] csrf_token in form? {request.form.get('csrf_token') is not None}"
)
try:
has_csrf_field = hasattr(form, 'csrf_token')
current_app.logger.debug(
f"[add_document] form has csrf field? {has_csrf_field}"
)
if has_csrf_field:
# Let op: we loggen geen tokenwaarde om lekken te vermijden; enkel aanwezigheid
current_app.logger.debug(
"[add_document] form.csrf_token field is present on form object"
)
# Bevestig of de CSRF-waarde effectief in de form is gebonden
try:
current_app.logger.debug(
f"[add_document] csrf bound? data_present={bool(form.csrf_token.data)} field_name={getattr(form.csrf_token, 'name', None)}"
)
except Exception:
pass
except Exception:
pass
except Exception:
pass
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
current_app.logger.info(f'Adding Document for {catalog_id}') current_app.logger.info(f'Adding Document for {catalog_id}')
@@ -400,6 +443,25 @@ def add_document():
except Exception as e: except Exception as e:
current_app.logger.error(f'Error adding document: {str(e)}') current_app.logger.error(f'Error adding document: {str(e)}')
flash('An error occurred while adding the document.', 'danger') flash('An error occurred while adding the document.', 'danger')
else:
# Toon en log validatiefouten als de submit faalt
if request.method == 'POST':
try:
current_app.logger.warning(
f"[add_document] form validation failed. errors={getattr(form, 'errors', {})}"
)
current_app.logger.debug(
f"[add_document] request.files keys after validation: {list(request.files.keys())}"
)
current_app.logger.debug(
f"[add_document] request.form keys after validation: {list(request.form.keys())}"
)
current_app.logger.debug(
f"[add_document] csrf_token in form after validation? {request.form.get('csrf_token') is not None}"
)
except Exception:
pass
form_validation_failed(request, form)
return render_template('document/add_document.html', form=form) return render_template('document/add_document.html', form=form)
@@ -407,7 +469,16 @@ def add_document():
@document_bp.route('/add_url', methods=['GET', 'POST']) @document_bp.route('/add_url', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def add_url(): def add_url():
form = AddURLForm(request.form) # Log vroege request-info om submitproblemen te diagnosticeren
try:
current_app.logger.debug(
f"[add_url] method={request.method}, content_type={request.content_type}, files_keys={list(request.files.keys())}"
)
except Exception:
pass
# Bind expliciet zowel form- als file-data (consistentie en duidelijkheid)
form = AddURLForm(CombinedMultiDict([request.form, request.files]))
catalog_id = session.get('catalog_id', None) catalog_id = session.get('catalog_id', None)
if catalog_id is None: if catalog_id is None:
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning') flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
@@ -417,6 +488,15 @@ def add_url():
if catalog.configuration and len(catalog.configuration) > 0: if catalog.configuration and len(catalog.configuration) > 0:
form.add_dynamic_fields("tagging_fields", catalog.configuration) form.add_dynamic_fields("tagging_fields", catalog.configuration)
url="" url=""
# Kleine debug om te zien of CSRF aan de form gebonden is
try:
if hasattr(form, 'csrf_token'):
current_app.logger.debug(
f"[add_url] csrf bound? data_present={bool(form.csrf_token.data)} field_name={getattr(form.csrf_token, 'name', None)}"
)
except Exception:
pass
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
@@ -462,6 +542,15 @@ def add_url():
except Exception as e: except Exception as e:
current_app.logger.error(f'Error adding document: {str(e)}') current_app.logger.error(f'Error adding document: {str(e)}')
flash('An error occurred while adding the document.', 'danger') flash('An error occurred while adding the document.', 'danger')
else:
if request.method == 'POST':
try:
current_app.logger.warning(
f"[add_url] form validation failed. errors={getattr(form, 'errors', {})}"
)
except Exception:
pass
form_validation_failed(request, form)
return render_template('document/add_url.html', form=form) return render_template('document/add_url.html', form=form)

View File

@@ -88,13 +88,13 @@ class BaseUserForm(FlaskForm):
last_name = StringField('Last Name', validators=[DataRequired(), Length(max=80)]) last_name = StringField('Last Name', validators=[DataRequired(), Length(max=80)])
valid_to = DateField('Valid to', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid to', id='form-control datepicker', validators=[Optional()])
tenant_id = IntegerField('Tenant ID', validators=[NumberRange(min=0)]) tenant_id = IntegerField('Tenant ID', validators=[NumberRange(min=0)])
roles = SelectMultipleField('Roles', coerce=int) selected_role_ids = SelectMultipleField('Roles', coerce=int)
is_primary_contact = BooleanField('Primary Contact') is_primary_contact = BooleanField('Primary Contact')
is_financial_contact = BooleanField('Financial Contact') is_financial_contact = BooleanField('Financial Contact')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BaseUserForm, self).__init__(*args, **kwargs) super(BaseUserForm, self).__init__(*args, **kwargs)
self.roles.choices = UserServices.get_assignable_roles() self.selected_role_ids.choices = UserServices.get_assignable_roles()
class CreateUserForm(BaseUserForm): class CreateUserForm(BaseUserForm):
@@ -177,7 +177,7 @@ def validate_make_name(form, field):
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.') raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.')
class TenantMakeForm(DynamicFormBase): class TenantMakeForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name]) name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
active = BooleanField('Active', validators=[Optional()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)

View File

@@ -217,21 +217,15 @@ def user():
if form.validate_on_submit(): if form.validate_on_submit():
current_app.logger.info(f"Adding User for tenant {session['tenant']['id']} ") current_app.logger.info(f"Adding User for tenant {session['tenant']['id']} ")
new_user = User(user_name=form.user_name.data, new_user = User()
email=form.email.data, form.populate_obj(new_user)
first_name=form.first_name.data,
last_name=form.last_name.data,
valid_to=form.valid_to.data,
tenant_id=form.tenant_id.data,
fs_uniquifier=uuid.uuid4().hex,
)
timestamp = dt.now(tz.utc) timestamp = dt.now(tz.utc)
new_user.created_at = timestamp new_user.created_at = timestamp
new_user.updated_at = timestamp new_user.updated_at = timestamp
# Add roles # Add roles
for role_id in form.roles.data: for role_id in form.selected_role_ids.data:
the_role = Role.query.get(role_id) the_role = Role.query.get(role_id)
new_user.roles.append(the_role) new_user.roles.append(the_role)
@@ -266,18 +260,18 @@ def user():
@roles_accepted('Super User', 'Tenant Admin', 'Partner Admin') @roles_accepted('Super User', 'Tenant Admin', 'Partner Admin')
def edit_user(user_id): def edit_user(user_id):
user = User.query.get_or_404(user_id) # This will return a 404 if no user is found user = User.query.get_or_404(user_id) # This will return a 404 if no user is found
tenant_id = session.get('tenant').get('id')
form = EditUserForm(obj=user) form = EditUserForm(obj=user)
if form.validate_on_submit(): if form.validate_on_submit():
# Populate the user with form data # Populate the user with form data
user.first_name = form.first_name.data form.populate_obj(user)
user.last_name = form.last_name.data timestamp = dt.now(tz.utc)
user.valid_to = form.valid_to.data user.updated_at = timestamp
user.updated_at = dt.now(tz.utc)
# Update roles # Update roles
current_roles = set(role.id for role in user.roles) current_roles = set(role.id for role in user.roles)
selected_roles = set(form.roles.data) selected_roles = set(form.selected_role_ids.data)
if UserServices.validate_role_assignments(selected_roles): if UserServices.validate_role_assignments(selected_roles):
# Add new roles # Add new roles
for role_id in selected_roles - current_roles: for role_id in selected_roles - current_roles:
@@ -303,7 +297,7 @@ def edit_user(user_id):
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
form.roles.data = [role.id for role in user.roles] form.selected_role_ids.data = [role.id for role in user.roles]
return render_template('user/edit_user.html', form=form, user_id=user_id) return render_template('user/edit_user.html', form=form, user_id=user_id)
@@ -577,10 +571,12 @@ def delete_tenant_project(tenant_project_id):
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_make(): def tenant_make():
form = TenantMakeForm() form = TenantMakeForm()
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION") current_app.logger.debug(f"ìn tenant_make view")
default_customisation_options = create_default_config_from_type_config(customisation_config["configuration"]) # customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
# default_customisation_options = create_default_config_from_type_config(customisation_config["configuration"])
if form.validate_on_submit(): if form.validate_on_submit():
current_app.logger.debug(f"in tenant_make form validate")
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
new_tenant_make = TenantMake() new_tenant_make = TenantMake()
form.populate_obj(new_tenant_make) form.populate_obj(new_tenant_make)
@@ -602,6 +598,8 @@ def tenant_make():
flash(f'Failed to add Tenant Make. Error: {e}', 'danger') flash(f'Failed to add Tenant Make. Error: {e}', 'danger')
current_app.logger.error(f'Failed to add Tenant Make {new_tenant_make.name}' current_app.logger.error(f'Failed to add Tenant Make {new_tenant_make.name}'
f'for tenant {tenant_id}. Error: {str(e)}') f'for tenant {tenant_id}. Error: {str(e)}')
else:
flash('Please fill in all required fields.', 'information')
return render_template('user/tenant_make.html', form=form) return render_template('user/tenant_make.html', form=form)

View File

@@ -0,0 +1,12 @@
INSUFFICIENT_INFORMATION_MESSAGES = [
"I'm afraid I don't have enough information to answer that properly. Feel free to ask something else!",
"There isnt enough data available right now to give you a clear answer. You're welcome to rephrase or ask a different question.",
"Sorry, I can't provide a complete answer based on the current information. Would you like to try asking something else?",
"I dont have enough details to give you a confident answer. You can always ask another question if youd like.",
"Unfortunately, I cant answer that accurately with the information at hand. Please feel free to ask something else.",
"Thats a great question, but I currently lack the necessary information to respond properly. Want to ask something different?",
"I wish I could help more, but the data I have isn't sufficient to answer this. Youre welcome to explore other questions.",
"Theres not enough context for me to provide a good answer. Dont hesitate to ask another question if you'd like!",
"I'm not able to give a definitive answer to that. Perhaps try a different question or angle?",
"Thanks for your question. At the moment, I cant give a solid answer — but I'm here if you want to ask something else!"
]

View File

@@ -19,19 +19,7 @@ from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpec
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
from eveai_chat_workers.outputs.globals.rag.rag_v1_0 import RAGOutput from eveai_chat_workers.outputs.globals.rag.rag_v1_0 import RAGOutput
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
from eveai_chat_workers.definitions.messages.globals.rag_messages import INSUFFICIENT_INFORMATION_MESSAGES
INSUFFICIENT_INFORMATION_MESSAGES = [
"I'm afraid I don't have enough information to answer that properly. Feel free to ask something else!",
"There isnt enough data available right now to give you a clear answer. You're welcome to rephrase or ask a different question.",
"Sorry, I can't provide a complete answer based on the current information. Would you like to try asking something else?",
"I dont have enough details to give you a confident answer. You can always ask another question if youd like.",
"Unfortunately, I cant answer that accurately with the information at hand. Please feel free to ask something else.",
"Thats a great question, but I currently lack the necessary information to respond properly. Want to ask something different?",
"I wish I could help more, but the data I have isn't sufficient to answer this. Youre welcome to explore other questions.",
"Theres not enough context for me to provide a good answer. Dont hesitate to ask another question if you'd like!",
"I'm not able to give a definitive answer to that. Perhaps try a different question or angle?",
"Thanks for your question. At the moment, I cant give a solid answer — but I'm here if you want to ask something else!"
]
class SpecialistExecutor(CrewAIBaseSpecialistExecutor): class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
""" """

View File

@@ -87,7 +87,6 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
return results return results
def execute_initial_state(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: def execute_initial_state(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
self.log_tuning("Traicie KO Criteria Interview Definition Specialist initial_state_execution started", {}) self.log_tuning("Traicie KO Criteria Interview Definition Specialist initial_state_execution started", {})

View File

@@ -25,6 +25,7 @@ from eveai_chat_workers.outputs.traicie.knockout_questions.knockout_questions_v1
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
from eveai_chat_workers.definitions.messages.globals.rag_messages import INSUFFICIENT_INFORMATION_MESSAGES
INITIALISATION_MESSAGES = [ INITIALISATION_MESSAGES = [
"Great! Let's see if this job might be a match for you by going through a few questions.", "Great! Let's see if this job might be a match for you by going through a few questions.",
@@ -85,18 +86,6 @@ TRY_TO_START_SELECTION_QUESTIONS = [
"Understood! However, we can't proceed without initiating the process. Would you like to start it now after all?", "Understood! However, we can't proceed without initiating the process. Would you like to start it now after all?",
"We appreciate your honesty. Just to clarify: the process only continues if we begin the selection. Shall we go ahead?" "We appreciate your honesty. Just to clarify: the process only continues if we begin the selection. Shall we go ahead?"
] ]
INSUFFICIENT_INFORMATION_MESSAGES = [
"I'm afraid I don't have enough information to answer that properly. Feel free to ask something else!",
"There isnt enough data available right now to give you a clear answer. You're welcome to rephrase or ask a different question.",
"Sorry, I can't provide a complete answer based on the current information. Would you like to try asking something else?",
"I dont have enough details to give you a confident answer. You can always ask another question if youd like.",
"Unfortunately, I cant answer that accurately with the information at hand. Please feel free to ask something else.",
"Thats a great question, but I currently lack the necessary information to respond properly. Want to ask something different?",
"I wish I could help more, but the data I have isn't sufficient to answer this. Youre welcome to explore other questions.",
"Theres not enough context for me to provide a good answer. Dont hesitate to ask another question if you'd like!",
"I'm not able to give a definitive answer to that. Perhaps try a different question or angle?",
"Thanks for your question. At the moment, I cant give a solid answer — but I'm here if you want to ask something else!"
]
KO_CRITERIA_NOT_MET_MESSAGES = [ KO_CRITERIA_NOT_MET_MESSAGES = [
"Thank you for your answers. Based on your responses, we won't be moving forward with this particular role. We do encourage you to keep an eye on our website for future opportunities.", "Thank you for your answers. Based on your responses, we won't be moving forward with this particular role. We do encourage you to keep an eye on our website for future opportunities.",
"We appreciate the time you took to answer our questions. At this point, we wont be proceeding with your application, but feel free to check our website regularly for new vacancies.", "We appreciate the time you took to answer our questions. At this point, we wont be proceeding with your application, but feel free to check our website regularly for new vacancies.",