Compare commits
10 Commits
v1.0.10-al
...
v1.0.13-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e77b478dd | ||
|
|
6f71259822 | ||
|
|
74cc7ae95e | ||
|
|
7f12c8b355 | ||
|
|
6069f5f7e5 | ||
|
|
3e644f1652 | ||
|
|
3316a8bc47 | ||
|
|
270479c77d | ||
|
|
0f4558d775 | ||
|
|
9f5f090f0c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,3 +42,4 @@ migrations/public/.DS_Store
|
||||
scripts/.DS_Store
|
||||
scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
||||
/eveai_repo.txt
|
||||
*repo.txt
|
||||
|
||||
@@ -15,7 +15,6 @@ migrations/
|
||||
nginx/mime.types
|
||||
*.gitignore*
|
||||
.python-version
|
||||
.repopackignore
|
||||
.repopackignore*
|
||||
repopack.config.json
|
||||
|
||||
|
||||
*repo.txt
|
||||
12
.repopackignore_components
Normal file
12
.repopackignore_components
Normal file
@@ -0,0 +1,12 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
12
.repopackignore_docker
Normal file
12
.repopackignore_docker
Normal file
@@ -0,0 +1,12 @@
|
||||
common/
|
||||
config/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
11
.repopackignore_eveai_api
Normal file
11
.repopackignore_eveai_api
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_eveai_app
Normal file
11
.repopackignore_eveai_app
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_eveai_beat
Normal file
11
.repopackignore_eveai_beat
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_eveai_chat
Normal file
11
.repopackignore_eveai_chat
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_eveai_chat_workers
Normal file
11
.repopackignore_eveai_chat_workers
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_eveai_entitlements
Normal file
11
.repopackignore_eveai_entitlements
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_eveai_workers
Normal file
11
.repopackignore_eveai_workers
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
instance/
|
||||
integrations/
|
||||
nginx/
|
||||
scripts/
|
||||
4
.repopackignore_full
Normal file
4
.repopackignore_full
Normal file
@@ -0,0 +1,4 @@
|
||||
docker
|
||||
integrations
|
||||
nginx
|
||||
scripts
|
||||
13
.repopackignore_integrations
Normal file
13
.repopackignore_integrations
Normal file
@@ -0,0 +1,13 @@
|
||||
common/
|
||||
config/
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
nginx/
|
||||
scripts/
|
||||
11
.repopackignore_nginx
Normal file
11
.repopackignore_nginx
Normal file
@@ -0,0 +1,11 @@
|
||||
docker/
|
||||
eveai_api/
|
||||
eveai_app/
|
||||
eveai_beat/
|
||||
eveai_chat/
|
||||
eveai_chat_workers/
|
||||
eveai_entitlements/
|
||||
eveai_workers/
|
||||
instance/
|
||||
integrations/
|
||||
scripts/
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -25,6 +25,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Security
|
||||
- In case of vulnerabilities.
|
||||
|
||||
## [1.0.13-alfa]
|
||||
|
||||
### Added
|
||||
- Finished Catalog introduction
|
||||
- Reinitialization of WordPress site for syncing
|
||||
|
||||
### Changed
|
||||
- Modification of WordPress Sync Component
|
||||
- Cleanup of attributes in Tenant
|
||||
|
||||
### Fixed
|
||||
- Overall bugfixes as result from the Catalog introduction
|
||||
|
||||
## [1.0.12-alfa]
|
||||
|
||||
### Added
|
||||
- Added Catalog functionality
|
||||
|
||||
### Changed
|
||||
- For changes in existing functionality.
|
||||
|
||||
### Deprecated
|
||||
- For soon-to-be removed features.
|
||||
|
||||
### Removed
|
||||
- For now removed features.
|
||||
|
||||
### Fixed
|
||||
- Set default language when registering Documents or URLs.
|
||||
|
||||
### Security
|
||||
- In case of vulnerabilities.
|
||||
|
||||
## [1.0.11-alfa]
|
||||
|
||||
### Added
|
||||
- License Usage Calculation realised
|
||||
- View License Usages
|
||||
- Celery Beat container added
|
||||
- First schedule in Celery Beat for calculating usage (hourly)
|
||||
|
||||
### Changed
|
||||
- repopack can now split for different components
|
||||
|
||||
### Fixed
|
||||
- Various fixes as consequence of changing file_location / file_name ==> bucket_name / object_name
|
||||
- Celery Routing / Queuing updated
|
||||
|
||||
## [1.0.10-alfa]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -38,7 +38,7 @@ class EveAIRetriever(BaseRetriever, BaseModel):
|
||||
similarity_threshold = self.model_variables['similarity_threshold']
|
||||
k = self.model_variables['k']
|
||||
|
||||
if self.tenant_info['rag_tuning']:
|
||||
if self.model_variables['rag_tuning']:
|
||||
try:
|
||||
current_date = get_date_in_timezone(self.tenant_info['timezone'])
|
||||
current_app.rag_tuning_logger.debug(f'Current date: {current_date}\n')
|
||||
@@ -73,7 +73,7 @@ class EveAIRetriever(BaseRetriever, BaseModel):
|
||||
current_app.logger.error(f'Error generating overview: {e}')
|
||||
db.session.rollback()
|
||||
|
||||
if self.tenant_info['rag_tuning']:
|
||||
if self.model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Parameters for Retrieval of documents: \n')
|
||||
current_app.rag_tuning_logger.debug(f'Similarity Threshold: {similarity_threshold}\n')
|
||||
current_app.rag_tuning_logger.debug(f'K: {k}\n')
|
||||
@@ -106,14 +106,14 @@ class EveAIRetriever(BaseRetriever, BaseModel):
|
||||
.limit(k)
|
||||
)
|
||||
|
||||
if self.tenant_info['rag_tuning']:
|
||||
if self.model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Query executed for Retrieval of documents: \n')
|
||||
current_app.rag_tuning_logger.debug(f'{query_obj.statement}\n')
|
||||
current_app.rag_tuning_logger.debug(f'---------------------------------------\n')
|
||||
|
||||
res = query_obj.all()
|
||||
|
||||
if self.tenant_info['rag_tuning']:
|
||||
if self.model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Retrieved {len(res)} relevant documents \n')
|
||||
current_app.rag_tuning_logger.debug(f'Data retrieved: \n')
|
||||
current_app.rag_tuning_logger.debug(f'{res}\n')
|
||||
@@ -121,7 +121,7 @@ class EveAIRetriever(BaseRetriever, BaseModel):
|
||||
|
||||
result = []
|
||||
for doc in res:
|
||||
if self.tenant_info['rag_tuning']:
|
||||
if self.model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Document ID: {doc[0].id} - Distance: {doc[1]}\n')
|
||||
current_app.rag_tuning_logger.debug(f'Chunk: \n {doc[0].chunk}\n\n')
|
||||
result.append(f'SOURCE: {doc[0].id}\n\n{doc[0].chunk}\n\n')
|
||||
|
||||
@@ -2,12 +2,49 @@ from common.extensions import db
|
||||
from .user import User, Tenant
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
class Catalog(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Embedding variables
|
||||
html_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'])
|
||||
html_end_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'li'])
|
||||
html_included_elements = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
html_excluded_elements = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
html_excluded_classes = db.Column(ARRAY(sa.String(200)), nullable=True)
|
||||
|
||||
min_chunk_size = db.Column(db.Integer, nullable=True, default=2000)
|
||||
max_chunk_size = db.Column(db.Integer, nullable=True, default=3000)
|
||||
|
||||
# Embedding search variables ==> move to specialist?
|
||||
es_k = db.Column(db.Integer, nullable=True, default=8)
|
||||
es_similarity_threshold = db.Column(db.Float, nullable=True, default=0.4)
|
||||
|
||||
# Chat variables ==> Move to Specialist?
|
||||
chat_RAG_temperature = db.Column(db.Float, nullable=True, default=0.3)
|
||||
chat_no_RAG_temperature = db.Column(db.Float, nullable=True, default=0.5)
|
||||
|
||||
# Tuning enablers
|
||||
embed_tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
rag_tuning = db.Column(db.Boolean, nullable=True, default=False) # Move to Specialist?
|
||||
|
||||
# Versioning Information
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
created_by = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True)
|
||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
|
||||
|
||||
|
||||
class Document(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# tenant_id = db.Column(db.Integer, db.ForeignKey(Tenant.id), nullable=False)
|
||||
catalog_id = db.Column(db.Integer, db.ForeignKey(Catalog.id), nullable=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
tenant_id = db.Column(db.Integer, db.ForeignKey(Tenant.id), nullable=False)
|
||||
valid_from = db.Column(db.DateTime, nullable=True)
|
||||
valid_to = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
@@ -56,12 +93,6 @@ class DocumentVersion(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<DocumentVersion {self.document_language.document_id}.{self.document_language.language}>.{self.id}>"
|
||||
|
||||
def calc_file_location(self):
|
||||
return f"{self.document.tenant_id}/{self.document.id}/{self.language}"
|
||||
|
||||
def calc_file_name(self):
|
||||
return f"{self.id}.{self.file_type}"
|
||||
|
||||
|
||||
class Embedding(db.Model):
|
||||
__tablename__ = 'embeddings'
|
||||
|
||||
@@ -94,8 +94,8 @@ class LicenseUsage(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
license_id = db.Column(db.Integer, db.ForeignKey('public.license.id'), nullable=False)
|
||||
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||
storage_mb_used = db.Column(db.Integer, default=0)
|
||||
embedding_mb_used = db.Column(db.Integer, default=0)
|
||||
storage_mb_used = db.Column(db.Float, default=0)
|
||||
embedding_mb_used = db.Column(db.Float, default=0)
|
||||
embedding_prompt_tokens_used = db.Column(db.Integer, default=0)
|
||||
embedding_completion_tokens_used = db.Column(db.Integer, default=0)
|
||||
embedding_total_tokens_used = db.Column(db.Integer, default=0)
|
||||
|
||||
@@ -34,32 +34,32 @@ class Tenant(db.Model):
|
||||
embedding_model = db.Column(db.String(50), nullable=True)
|
||||
llm_model = db.Column(db.String(50), nullable=True)
|
||||
|
||||
# Embedding variables
|
||||
html_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'])
|
||||
html_end_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'li'])
|
||||
html_included_elements = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
html_excluded_elements = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
html_excluded_classes = db.Column(ARRAY(sa.String(200)), nullable=True)
|
||||
|
||||
min_chunk_size = db.Column(db.Integer, nullable=True, default=2000)
|
||||
max_chunk_size = db.Column(db.Integer, nullable=True, default=3000)
|
||||
|
||||
# Embedding search variables
|
||||
es_k = db.Column(db.Integer, nullable=True, default=5)
|
||||
es_similarity_threshold = db.Column(db.Float, nullable=True, default=0.7)
|
||||
|
||||
# Chat variables
|
||||
chat_RAG_temperature = db.Column(db.Float, nullable=True, default=0.3)
|
||||
chat_no_RAG_temperature = db.Column(db.Float, nullable=True, default=0.5)
|
||||
# # Embedding variables ==> To be removed once all migrations (dev + prod) have been done
|
||||
# html_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'])
|
||||
# html_end_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'li'])
|
||||
# html_included_elements = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
# html_excluded_elements = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
# html_excluded_classes = db.Column(ARRAY(sa.String(200)), nullable=True)
|
||||
#
|
||||
# min_chunk_size = db.Column(db.Integer, nullable=True, default=2000)
|
||||
# max_chunk_size = db.Column(db.Integer, nullable=True, default=3000)
|
||||
#
|
||||
# # Embedding search variables
|
||||
# es_k = db.Column(db.Integer, nullable=True, default=5)
|
||||
# es_similarity_threshold = db.Column(db.Float, nullable=True, default=0.7)
|
||||
#
|
||||
# # Chat variables
|
||||
# chat_RAG_temperature = db.Column(db.Float, nullable=True, default=0.3)
|
||||
# chat_no_RAG_temperature = db.Column(db.Float, nullable=True, default=0.5)
|
||||
fallback_algorithms = db.Column(ARRAY(sa.String(50)), nullable=True)
|
||||
|
||||
# Licensing Information
|
||||
encrypted_chat_api_key = db.Column(db.String(500), nullable=True)
|
||||
encrypted_api_key = db.Column(db.String(500), nullable=True)
|
||||
|
||||
# Tuning enablers
|
||||
embed_tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
rag_tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
# # Tuning enablers
|
||||
# embed_tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
# rag_tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
|
||||
# Entitlements
|
||||
currency = db.Column(db.String(20), nullable=True)
|
||||
@@ -96,20 +96,7 @@ class Tenant(db.Model):
|
||||
'allowed_languages': self.allowed_languages,
|
||||
'embedding_model': self.embedding_model,
|
||||
'llm_model': self.llm_model,
|
||||
'html_tags': self.html_tags,
|
||||
'html_end_tags': self.html_end_tags,
|
||||
'html_included_elements': self.html_included_elements,
|
||||
'html_excluded_elements': self.html_excluded_elements,
|
||||
'html_excluded_classes': self.html_excluded_classes,
|
||||
'min_chunk_size': self.min_chunk_size,
|
||||
'max_chunk_size': self.max_chunk_size,
|
||||
'es_k': self.es_k,
|
||||
'es_similarity_threshold': self.es_similarity_threshold,
|
||||
'chat_RAG_temperature': self.chat_RAG_temperature,
|
||||
'chat_no_RAG_temperature': self.chat_no_RAG_temperature,
|
||||
'fallback_algorithms': self.fallback_algorithms,
|
||||
'embed_tuning': self.embed_tuning,
|
||||
'rag_tuning': self.rag_tuning,
|
||||
'currency': self.currency,
|
||||
'usage_email': self.usage_email,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from celery import Celery
|
||||
from kombu import Queue
|
||||
from werkzeug.local import LocalProxy
|
||||
from redbeat import RedBeatScheduler
|
||||
|
||||
celery_app = Celery()
|
||||
|
||||
|
||||
def init_celery(celery, app):
|
||||
def init_celery(celery, app, is_beat=False):
|
||||
celery_app.main = app.name
|
||||
app.logger.debug(f'CELERY_BROKER_URL: {app.config["CELERY_BROKER_URL"]}')
|
||||
app.logger.debug(f'CELERY_RESULT_BACKEND: {app.config["CELERY_RESULT_BACKEND"]}')
|
||||
|
||||
celery_config = {
|
||||
'broker_url': app.config.get('CELERY_BROKER_URL', 'redis://localhost:6379/0'),
|
||||
'result_backend': app.config.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0'),
|
||||
@@ -17,19 +19,40 @@ def init_celery(celery, app):
|
||||
'accept_content': app.config.get('CELERY_ACCEPT_CONTENT', ['json']),
|
||||
'timezone': app.config.get('CELERY_TIMEZONE', 'UTC'),
|
||||
'enable_utc': app.config.get('CELERY_ENABLE_UTC', True),
|
||||
'task_routes': {'eveai_worker.tasks.create_embeddings': {'queue': 'embeddings',
|
||||
'routing_key': 'embeddings.create_embeddings'}},
|
||||
}
|
||||
|
||||
if is_beat:
|
||||
# Add configurations specific to Beat scheduler
|
||||
celery_config['beat_scheduler'] = 'redbeat.RedBeatScheduler'
|
||||
celery_config['redbeat_lock_key'] = 'redbeat::lock'
|
||||
celery_config['beat_max_loop_interval'] = 10 # Adjust as needed
|
||||
|
||||
celery_app.conf.update(**celery_config)
|
||||
|
||||
# Setting up Celery task queues
|
||||
celery_app.conf.task_queues = (
|
||||
Queue('default', routing_key='task.#'),
|
||||
Queue('embeddings', routing_key='embeddings.#', queue_arguments={'x-max-priority': 10}),
|
||||
Queue('llm_interactions', routing_key='llm_interactions.#', queue_arguments={'x-max-priority': 5}),
|
||||
)
|
||||
# Task queues for workers only
|
||||
if not is_beat:
|
||||
celery_app.conf.task_queues = (
|
||||
Queue('default', routing_key='task.#'),
|
||||
Queue('embeddings', routing_key='embeddings.#', queue_arguments={'x-max-priority': 10}),
|
||||
Queue('llm_interactions', routing_key='llm_interactions.#', queue_arguments={'x-max-priority': 5}),
|
||||
Queue('entitlements', routing_key='entitlements.#', queue_arguments={'x-max-priority': 10}),
|
||||
)
|
||||
celery_app.conf.task_routes = {
|
||||
'eveai_workers.*': { # All tasks from eveai_workers module
|
||||
'queue': 'embeddings',
|
||||
'routing_key': 'embeddings.#',
|
||||
},
|
||||
'eveai_chat_workers.*': { # All tasks from eveai_chat_workers module
|
||||
'queue': 'llm_interactions',
|
||||
'routing_key': 'llm_interactions.#',
|
||||
},
|
||||
'eveai_entitlements.*': { # All tasks from eveai_entitlements module
|
||||
'queue': 'entitlements',
|
||||
'routing_key': 'entitlements.#',
|
||||
}
|
||||
}
|
||||
|
||||
# Ensuring tasks execute with Flask application context
|
||||
# Ensure tasks execute with Flask context
|
||||
class ContextTask(celery.Task):
|
||||
def __call__(self, *args, **kwargs):
|
||||
with app.app_context():
|
||||
@@ -37,6 +60,39 @@ def init_celery(celery, app):
|
||||
|
||||
celery.Task = ContextTask
|
||||
|
||||
# Original init_celery before updating for beat
|
||||
# def init_celery(celery, app):
|
||||
# celery_app.main = app.name
|
||||
# app.logger.debug(f'CELERY_BROKER_URL: {app.config["CELERY_BROKER_URL"]}')
|
||||
# app.logger.debug(f'CELERY_RESULT_BACKEND: {app.config["CELERY_RESULT_BACKEND"]}')
|
||||
# celery_config = {
|
||||
# 'broker_url': app.config.get('CELERY_BROKER_URL', 'redis://localhost:6379/0'),
|
||||
# 'result_backend': app.config.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0'),
|
||||
# 'task_serializer': app.config.get('CELERY_TASK_SERIALIZER', 'json'),
|
||||
# 'result_serializer': app.config.get('CELERY_RESULT_SERIALIZER', 'json'),
|
||||
# 'accept_content': app.config.get('CELERY_ACCEPT_CONTENT', ['json']),
|
||||
# 'timezone': app.config.get('CELERY_TIMEZONE', 'UTC'),
|
||||
# 'enable_utc': app.config.get('CELERY_ENABLE_UTC', True),
|
||||
# 'task_routes': {'eveai_worker.tasks.create_embeddings': {'queue': 'embeddings',
|
||||
# 'routing_key': 'embeddings.create_embeddings'}},
|
||||
# }
|
||||
# celery_app.conf.update(**celery_config)
|
||||
#
|
||||
# # Setting up Celery task queues
|
||||
# celery_app.conf.task_queues = (
|
||||
# Queue('default', routing_key='task.#'),
|
||||
# Queue('embeddings', routing_key='embeddings.#', queue_arguments={'x-max-priority': 10}),
|
||||
# Queue('llm_interactions', routing_key='llm_interactions.#', queue_arguments={'x-max-priority': 5}),
|
||||
# )
|
||||
#
|
||||
# # Ensuring tasks execute with Flask application context
|
||||
# class ContextTask(celery.Task):
|
||||
# def __call__(self, *args, **kwargs):
|
||||
# with app.app_context():
|
||||
# return self.run(*args, **kwargs)
|
||||
#
|
||||
# celery.Task = ContextTask
|
||||
|
||||
|
||||
def make_celery(app_name, config):
|
||||
return celery_app
|
||||
|
||||
@@ -17,11 +17,12 @@ from ..models.user import Tenant
|
||||
|
||||
def create_document_stack(api_input, file, filename, extension, tenant_id):
|
||||
# Create the Document
|
||||
new_doc = create_document(api_input, filename, tenant_id)
|
||||
catalog_id = int(api_input.get('catalog_id'))
|
||||
new_doc = create_document(api_input, filename, catalog_id)
|
||||
db.session.add(new_doc)
|
||||
|
||||
# Create the DocumentVersion
|
||||
new_doc_vers = create_version_for_document(new_doc,
|
||||
new_doc_vers = create_version_for_document(new_doc, tenant_id,
|
||||
api_input.get('url', ''),
|
||||
api_input.get('language', 'en'),
|
||||
api_input.get('user_context', ''),
|
||||
@@ -45,7 +46,7 @@ def create_document_stack(api_input, file, filename, extension, tenant_id):
|
||||
return new_doc, new_doc_vers
|
||||
|
||||
|
||||
def create_document(form, filename, tenant_id):
|
||||
def create_document(form, filename, catalog_id):
|
||||
new_doc = Document()
|
||||
if form['name'] == '':
|
||||
new_doc.name = filename.rsplit('.', 1)[0]
|
||||
@@ -56,13 +57,13 @@ def create_document(form, filename, tenant_id):
|
||||
new_doc.valid_from = form['valid_from']
|
||||
else:
|
||||
new_doc.valid_from = dt.now(tz.utc)
|
||||
new_doc.tenant_id = tenant_id
|
||||
new_doc.catalog_id = catalog_id
|
||||
set_logging_information(new_doc, dt.now(tz.utc))
|
||||
|
||||
return new_doc
|
||||
|
||||
|
||||
def create_version_for_document(document, url, language, user_context, user_metadata):
|
||||
def create_version_for_document(document, tenant_id, url, language, user_context, user_metadata):
|
||||
new_doc_vers = DocumentVersion()
|
||||
if url != '':
|
||||
new_doc_vers.url = url
|
||||
@@ -82,7 +83,7 @@ def create_version_for_document(document, url, language, user_context, user_meta
|
||||
|
||||
set_logging_information(new_doc_vers, dt.now(tz.utc))
|
||||
|
||||
mark_tenant_storage_dirty(document.tenant_id)
|
||||
mark_tenant_storage_dirty(tenant_id)
|
||||
|
||||
return new_doc_vers
|
||||
|
||||
@@ -99,12 +100,12 @@ def upload_file_for_version(doc_vers, file, extension, tenant_id):
|
||||
doc_vers.doc_id,
|
||||
doc_vers.language,
|
||||
doc_vers.id,
|
||||
doc_vers.file_name,
|
||||
f"{doc_vers.id}.{extension}",
|
||||
file
|
||||
)
|
||||
doc_vers.bucket_name = bn
|
||||
doc_vers.object_name = on
|
||||
doc_vers.file_size_mb = size / 1048576 # Convert bytes to MB
|
||||
doc_vers.file_size = size / 1048576 # Convert bytes to MB
|
||||
|
||||
db.session.commit()
|
||||
current_app.logger.info(f'Successfully saved document to MinIO for tenant {tenant_id} for '
|
||||
@@ -222,10 +223,9 @@ def process_multiple_urls(urls, tenant_id, api_input):
|
||||
|
||||
|
||||
def start_embedding_task(tenant_id, doc_vers_id):
|
||||
task = current_celery.send_task('create_embeddings', queue='embeddings', args=[
|
||||
tenant_id,
|
||||
doc_vers_id,
|
||||
])
|
||||
task = current_celery.send_task('create_embeddings',
|
||||
args=[tenant_id, doc_vers_id,],
|
||||
queue='embeddings')
|
||||
current_app.logger.info(f'Embedding creation started for tenant {tenant_id}, '
|
||||
f'Document Version {doc_vers_id}. '
|
||||
f'Embedding creation task: {task.id}')
|
||||
@@ -287,7 +287,7 @@ def edit_document_version(version_id, user_context):
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def refresh_document_with_info(doc_id, api_input):
|
||||
def refresh_document_with_info(doc_id, tenant_id, api_input):
|
||||
doc = Document.query.get_or_404(doc_id)
|
||||
old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first()
|
||||
|
||||
@@ -295,11 +295,11 @@ def refresh_document_with_info(doc_id, api_input):
|
||||
return None, "This document has no URL. Only documents with a URL can be refreshed."
|
||||
|
||||
new_doc_vers = create_version_for_document(
|
||||
doc,
|
||||
doc, tenant_id,
|
||||
old_doc_vers.url,
|
||||
api_input.get('language', old_doc_vers.language),
|
||||
api_input.get('user_context', old_doc_vers.user_context),
|
||||
api_input.get('user_metadata', old_doc_vers.user_metadata)
|
||||
api_input.get('user_metadata', old_doc_vers.user_metadata),
|
||||
)
|
||||
|
||||
set_logging_information(new_doc_vers, dt.now(tz.utc))
|
||||
@@ -319,18 +319,18 @@ def refresh_document_with_info(doc_id, api_input):
|
||||
response.raise_for_status()
|
||||
file_content = response.content
|
||||
|
||||
upload_file_for_version(new_doc_vers, file_content, extension, doc.tenant_id)
|
||||
upload_file_for_version(new_doc_vers, file_content, extension, tenant_id)
|
||||
|
||||
task = current_celery.send_task('create_embeddings', queue='embeddings', args=[
|
||||
doc.tenant_id,
|
||||
new_doc_vers.id,
|
||||
])
|
||||
task = current_celery.send_task('create_embeddings', args=[tenant_id, new_doc_vers.id,], queue='embeddings')
|
||||
current_app.logger.info(f'Embedding creation started for document {doc_id} on version {new_doc_vers.id} '
|
||||
f'with task id: {task.id}.')
|
||||
|
||||
return new_doc_vers, task.id
|
||||
|
||||
|
||||
# Update the existing refresh_document function to use the new refresh_document_with_info
|
||||
def refresh_document(doc_id):
|
||||
def refresh_document(doc_id, tenant_id):
|
||||
current_app.logger.info(f'Refreshing document {doc_id}')
|
||||
doc = Document.query.get_or_404(doc_id)
|
||||
old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first()
|
||||
|
||||
@@ -340,11 +340,11 @@ def refresh_document(doc_id):
|
||||
'user_metadata': old_doc_vers.user_metadata
|
||||
}
|
||||
|
||||
return refresh_document_with_info(doc_id, api_input)
|
||||
return refresh_document_with_info(doc_id, tenant_id, api_input)
|
||||
|
||||
|
||||
# Function triggered when a document_version is created or updated
|
||||
def mark_tenant_storage_dirty(tenant_id):
|
||||
tenant = db.session.query(Tenant).filter_by(id=tenant_id).first()
|
||||
tenant = db.session.query(Tenant).filter_by(id=int(tenant_id)).first()
|
||||
tenant.storage_dirty = True
|
||||
db.session.commit()
|
||||
|
||||
@@ -54,9 +54,7 @@ class MinioClient:
|
||||
except S3Error as err:
|
||||
raise Exception(f"Error occurred while uploading file: {err}")
|
||||
|
||||
def download_document_file(self, tenant_id, document_id, language, version_id, filename):
|
||||
bucket_name = self.generate_bucket_name(tenant_id)
|
||||
object_name = self.generate_object_name(document_id, language, version_id, filename)
|
||||
def download_document_file(self, tenant_id, bucket_name, object_name):
|
||||
try:
|
||||
response = self.client.get_object(bucket_name, object_name)
|
||||
return response.read()
|
||||
|
||||
@@ -14,7 +14,7 @@ from portkey_ai.langchain.portkey_langchain_callback_handler import LangchainCal
|
||||
from common.langchain.llm_metrics_handler import LLMMetricsHandler
|
||||
from common.langchain.tracked_openai_embeddings import TrackedOpenAIEmbeddings
|
||||
from common.langchain.tracked_transcribe import tracked_transcribe
|
||||
from common.models.document import EmbeddingSmallOpenAI, EmbeddingLargeOpenAI
|
||||
from common.models.document import EmbeddingSmallOpenAI, EmbeddingLargeOpenAI, Catalog
|
||||
from common.models.user import Tenant
|
||||
from config.model_config import MODEL_CONFIG
|
||||
from common.utils.business_event_context import current_event
|
||||
@@ -42,8 +42,9 @@ def set_language_prompt_template(cls, language_prompt):
|
||||
|
||||
|
||||
class ModelVariables(MutableMapping):
|
||||
def __init__(self, tenant: Tenant):
|
||||
def __init__(self, tenant: Tenant, catalog_id=None):
|
||||
self.tenant = tenant
|
||||
self.catalog_id = catalog_id
|
||||
self._variables = self._initialize_variables()
|
||||
self._embedding_model = None
|
||||
self._llm = None
|
||||
@@ -57,25 +58,32 @@ class ModelVariables(MutableMapping):
|
||||
def _initialize_variables(self):
|
||||
variables = {}
|
||||
|
||||
# We initialize the variables that are available knowing the tenant. For the other, we will apply 'lazy loading'
|
||||
variables['k'] = self.tenant.es_k or 5
|
||||
variables['similarity_threshold'] = self.tenant.es_similarity_threshold or 0.7
|
||||
variables['RAG_temperature'] = self.tenant.chat_RAG_temperature or 0.3
|
||||
variables['no_RAG_temperature'] = self.tenant.chat_no_RAG_temperature or 0.5
|
||||
variables['embed_tuning'] = self.tenant.embed_tuning or False
|
||||
variables['rag_tuning'] = self.tenant.rag_tuning or False
|
||||
# Get the Catalog if catalog_id is passed
|
||||
if self.catalog_id:
|
||||
catalog = Catalog.query.get_or_404(self.catalog_id)
|
||||
|
||||
# We initialize the variables that are available knowing the tenant.
|
||||
variables['embed_tuning'] = catalog.embed_tuning or False
|
||||
|
||||
# Set HTML Chunking Variables
|
||||
variables['html_tags'] = catalog.html_tags
|
||||
variables['html_end_tags'] = catalog.html_end_tags
|
||||
variables['html_included_elements'] = catalog.html_included_elements
|
||||
variables['html_excluded_elements'] = catalog.html_excluded_elements
|
||||
variables['html_excluded_classes'] = catalog.html_excluded_classes
|
||||
|
||||
# Set Chunk Size variables
|
||||
variables['min_chunk_size'] = catalog.min_chunk_size
|
||||
variables['max_chunk_size'] = catalog.max_chunk_size
|
||||
|
||||
# Set the RAG Context (will have to change once specialists are defined
|
||||
variables['rag_context'] = self.tenant.rag_context or " "
|
||||
|
||||
# Set HTML Chunking Variables
|
||||
variables['html_tags'] = self.tenant.html_tags
|
||||
variables['html_end_tags'] = self.tenant.html_end_tags
|
||||
variables['html_included_elements'] = self.tenant.html_included_elements
|
||||
variables['html_excluded_elements'] = self.tenant.html_excluded_elements
|
||||
variables['html_excluded_classes'] = self.tenant.html_excluded_classes
|
||||
|
||||
# Set Chunk Size variables
|
||||
variables['min_chunk_size'] = self.tenant.min_chunk_size
|
||||
variables['max_chunk_size'] = self.tenant.max_chunk_size
|
||||
# Temporary setting until we have Specialists
|
||||
variables['rag_tuning'] = False
|
||||
variables['RAG_temperature'] = 0.3
|
||||
variables['no_RAG_temperature'] = 0.5
|
||||
variables['k'] = 8
|
||||
variables['similarity_threshold'] = 0.4
|
||||
|
||||
# Set model providers
|
||||
variables['embedding_provider'], variables['embedding_model'] = self.tenant.embedding_model.rsplit('.', 1)
|
||||
@@ -195,7 +203,12 @@ class ModelVariables(MutableMapping):
|
||||
return self.transcription_client
|
||||
elif key in self._variables.get('prompt_templates', []):
|
||||
return self.get_prompt_template(key)
|
||||
return self._variables.get(key)
|
||||
else:
|
||||
value = self._variables.get(key)
|
||||
if value is not None:
|
||||
return value
|
||||
else:
|
||||
raise KeyError(f'Variable {key} does not exist in ModelVariables')
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
self._variables[key] = value
|
||||
@@ -225,8 +238,8 @@ class ModelVariables(MutableMapping):
|
||||
return self._variables.values()
|
||||
|
||||
|
||||
def select_model_variables(tenant):
|
||||
model_variables = ModelVariables(tenant=tenant)
|
||||
def select_model_variables(tenant, catalog_id=None):
|
||||
model_variables = ModelVariables(tenant=tenant, catalog_id=catalog_id)
|
||||
return model_variables
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ def prefixed_url_for(endpoint, **values):
|
||||
prefix = request.headers.get('X-Forwarded-Prefix', '')
|
||||
scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
|
||||
host = request.headers.get('Host', request.host)
|
||||
current_app.logger.debug(f'prefix: {prefix}, scheme: {scheme}, host: {host}')
|
||||
|
||||
external = values.pop('_external', False)
|
||||
generated_url = url_for(endpoint, **values)
|
||||
|
||||
@@ -37,7 +37,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_app.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -45,7 +45,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_workers.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -53,7 +53,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_chat.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -61,7 +61,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_chat_workers.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -69,7 +69,23 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_api.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
'file_beat': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_beat.log',
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
'file_entitlements': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/eveai_entitlements.log',
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -77,7 +93,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/sqlalchemy.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -85,7 +101,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/mailman.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -93,7 +109,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/security.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -101,7 +117,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/rag_tuning.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -109,7 +125,7 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/embed_tuning.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -117,7 +133,7 @@ LOGGING = {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': 'logs/business_events.log',
|
||||
'maxBytes': 1024 * 1024 * 5, # 5MB
|
||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||
'backupCount': 10,
|
||||
'formatter': 'standard',
|
||||
},
|
||||
@@ -172,6 +188,16 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
'eveai_beat': { # logger for the eveai_beat
|
||||
'handlers': ['file_beat', 'graylog', ] if env == 'production' else ['file_beat', ],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
'eveai_entitlements': { # logger for the eveai_entitlements
|
||||
'handlers': ['file_entitlements', 'graylog', ] if env == 'production' else ['file_entitlements', ],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
'sqlalchemy.engine': { # logger for the sqlalchemy
|
||||
'handlers': ['file_sqlalchemy', 'graylog', ] if env == 'production' else ['file_sqlalchemy', ],
|
||||
'level': 'DEBUG',
|
||||
|
||||
@@ -231,6 +231,59 @@ services:
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
eveai_beat:
|
||||
image: josakola/eveai_beat:latest
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./docker/eveai_beat/Dockerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
environment:
|
||||
<<: *common-variables
|
||||
COMPONENT_NAME: eveai_beat
|
||||
volumes:
|
||||
- ../eveai_beat:/app/eveai_beat
|
||||
- ../common:/app/common
|
||||
- ../config:/app/config
|
||||
- ../scripts:/app/scripts
|
||||
- ../patched_packages:/app/patched_packages
|
||||
- eveai_logs:/app/logs
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
eveai_entitlements:
|
||||
image: josakola/eveai_entitlements:latest
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./docker/eveai_entitlements/Dockerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
environment:
|
||||
<<: *common-variables
|
||||
COMPONENT_NAME: eveai_entitlements
|
||||
volumes:
|
||||
- ../eveai_entitlements:/app/eveai_entitlements
|
||||
- ../common:/app/common
|
||||
- ../config:/app/config
|
||||
- ../scripts:/app/scripts
|
||||
- ../patched_packages:/app/patched_packages
|
||||
- eveai_logs:/app/logs
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
|
||||
db:
|
||||
hostname: db
|
||||
image: ankane/pgvector
|
||||
|
||||
@@ -145,6 +145,28 @@ services:
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
eveai_beat:
|
||||
platform: linux/amd64
|
||||
image: josakola/eveai_beat:latest
|
||||
environment:
|
||||
<<: *common-variables
|
||||
COMPONENT_NAME: eveai_beat
|
||||
volumes:
|
||||
- eveai_logs:/app/logs
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
eveai_entitlements:
|
||||
platform: linux/amd64
|
||||
image: josakola/eveai_entitlements:latest
|
||||
environment:
|
||||
<<: *common-variables
|
||||
COMPONENT_NAME: eveai_entitlements
|
||||
volumes:
|
||||
- eveai_logs:/app/logs
|
||||
networks:
|
||||
- eveai-network
|
||||
|
||||
flower:
|
||||
image: josakola/flower:latest
|
||||
environment:
|
||||
|
||||
65
docker/eveai_beat/Dockerfile
Normal file
65
docker/eveai_beat/Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
||||
ARG PYTHON_VERSION=3.12.3
|
||||
FROM python:${PYTHON_VERSION}-slim as base
|
||||
|
||||
# Prevents Python from writing pyc files.
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||
# the application crashes without emitting any logs due to buffering.
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Create directory for patched packages and set permissions
|
||||
RUN mkdir -p /app/patched_packages && \
|
||||
chmod 777 /app/patched_packages
|
||||
|
||||
# Ensure patches are applied to the application.
|
||||
ENV PYTHONPATH=/app/patched_packages:$PYTHONPATH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||
ARG UID=10001
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/nonexistent" \
|
||||
--shell "/bin/bash" \
|
||||
--no-create-home \
|
||||
--uid "${UID}" \
|
||||
appuser
|
||||
|
||||
# Install necessary packages and build tools
|
||||
#RUN apt-get update && apt-get install -y \
|
||||
# build-essential \
|
||||
# gcc \
|
||||
# && apt-get clean \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create logs directory and set permissions
|
||||
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
||||
|
||||
# Install Python dependencies.
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||
# into this layer.
|
||||
|
||||
COPY requirements.txt /app/
|
||||
RUN python -m pip install -r /app/requirements.txt
|
||||
|
||||
# Copy the source code into the container.
|
||||
COPY eveai_beat /app/eveai_beat
|
||||
COPY common /app/common
|
||||
COPY config /app/config
|
||||
COPY scripts /app/scripts
|
||||
COPY patched_packages /app/patched_packages
|
||||
COPY --chown=root:root scripts/entrypoint_no_db.sh /app/scripts/
|
||||
|
||||
# Set ownership of the application directory to the non-privileged user
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["/app/scripts/entrypoint_no_db.sh"]
|
||||
CMD ["/app/scripts/start_eveai_beat.sh"]
|
||||
69
docker/eveai_entitlements/Dockerfile
Normal file
69
docker/eveai_entitlements/Dockerfile
Normal file
@@ -0,0 +1,69 @@
|
||||
ARG PYTHON_VERSION=3.12.3
|
||||
FROM python:${PYTHON_VERSION}-slim as base
|
||||
|
||||
# Prevents Python from writing pyc files.
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||
# the application crashes without emitting any logs due to buffering.
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Create directory for patched packages and set permissions
|
||||
RUN mkdir -p /app/patched_packages && \
|
||||
chmod 777 /app/patched_packages
|
||||
|
||||
# Ensure patches are applied to the application.
|
||||
ENV PYTHONPATH=/app/patched_packages:$PYTHONPATH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||
ARG UID=10001
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/nonexistent" \
|
||||
--shell "/bin/bash" \
|
||||
--no-create-home \
|
||||
--uid "${UID}" \
|
||||
appuser
|
||||
|
||||
# Install necessary packages and build tools
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create logs directory and set permissions
|
||||
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
||||
|
||||
# Install Python dependencies.
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||
# into this layer.
|
||||
|
||||
COPY requirements.txt /app/
|
||||
RUN python -m pip install -r /app/requirements.txt
|
||||
|
||||
# Copy the source code into the container.
|
||||
COPY eveai_entitlements /app/eveai_entitlements
|
||||
COPY common /app/common
|
||||
COPY config /app/config
|
||||
COPY scripts /app/scripts
|
||||
COPY patched_packages /app/patched_packages
|
||||
COPY --chown=root:root scripts/entrypoint.sh /app/scripts/
|
||||
|
||||
# Set permissions for entrypoint script
|
||||
RUN chmod 777 /app/scripts/entrypoint.sh
|
||||
|
||||
# Set ownership of the application directory to the non-privileged user
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
|
||||
CMD ["/app/scripts/start_eveai_entitlements.sh"]
|
||||
60
docker/release_and_tag_eveai.sh
Normal file
60
docker/release_and_tag_eveai.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Initialize variables
|
||||
RELEASE_VERSION=""
|
||||
RELEASE_MESSAGE=""
|
||||
DOCKER_ACCOUNT="josakola" # Your Docker account name
|
||||
|
||||
# Parse input arguments
|
||||
while getopts r:m: flag
|
||||
do
|
||||
case "${flag}" in
|
||||
r) RELEASE_VERSION=${OPTARG};;
|
||||
m) RELEASE_MESSAGE=${OPTARG};;
|
||||
*)
|
||||
echo "Usage: $0 -r <release_version> -m <release_message>"
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Ensure both version and message are provided
|
||||
if [ -z "$RELEASE_VERSION" ]; then
|
||||
echo "Error: Release version not provided. Use -r <release_version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$RELEASE_MESSAGE" ]; then
|
||||
echo "Error: Release message not provided. Use -m <release_message>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Path to your docker-compose file
|
||||
DOCKER_COMPOSE_FILE="compose_dev.yaml"
|
||||
|
||||
# Get all the services defined in the docker-compose file
|
||||
SERVICES=$(docker-compose -f $DOCKER_COMPOSE_FILE config --services)
|
||||
|
||||
# Tag and push images for all services that belong to your Docker account
|
||||
for SERVICE in $SERVICES; do
|
||||
DOCKER_IMAGE="your-docker-repo/$SERVICE"
|
||||
|
||||
# Check if the image starts with your Docker account name
|
||||
if [[ $DOCKER_IMAGE == $DOCKER_ACCOUNT* ]]; then
|
||||
echo "Tagging Docker image for service: $SERVICE with version: $RELEASE_VERSION"
|
||||
|
||||
# Tag the 'latest' image with the new release version
|
||||
docker tag $DOCKER_IMAGE:latest $DOCKER_IMAGE:$RELEASE_VERSION
|
||||
|
||||
# Push the newly tagged image to Docker Hub
|
||||
docker push $DOCKER_IMAGE:$RELEASE_VERSION
|
||||
else
|
||||
echo "Skipping service: $SERVICE (not part of Docker account $DOCKER_ACCOUNT)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 3: Tag the Git repository with the release version
|
||||
echo "Tagging Git repository with version: $RELEASE_VERSION"
|
||||
git tag -a v$RELEASE_VERSION -m "Release $RELEASE_VERSION: $RELEASE_MESSAGE"
|
||||
git push origin v$RELEASE_VERSION
|
||||
|
||||
echo "Release process completed for version: $RELEASE_VERSION"
|
||||
@@ -56,13 +56,6 @@ def create_app(config_file=None):
|
||||
app.logger.debug(f'Request URL: {request.url}')
|
||||
app.logger.debug(f'Request headers: {dict(request.headers)}')
|
||||
|
||||
# Log request arguments
|
||||
app.logger.debug(f'Request args: {request.args}')
|
||||
|
||||
# Log form data if it's a POST request
|
||||
if request.method == 'POST':
|
||||
app.logger.debug(f'Form data: {request.form}')
|
||||
|
||||
# Log JSON data if the content type is application/json
|
||||
if request.is_json:
|
||||
app.logger.debug(f'JSON data: {request.json}')
|
||||
@@ -95,6 +88,10 @@ def create_app(config_file=None):
|
||||
# Don't raise the exception here, let the request continue
|
||||
# The appropriate error handling will be done in the specific endpoints
|
||||
|
||||
@app.route('/api/v1')
|
||||
def swagger():
|
||||
return api_rest.render_doc()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ document_ns = Namespace('documents', description='Document related operations')
|
||||
|
||||
# Define models for request parsing and response serialization
|
||||
upload_parser = reqparse.RequestParser()
|
||||
upload_parser.add_argument('catalog_id', location='form', type=int, required=True, help='The catalog to add the file to')
|
||||
upload_parser.add_argument('file', location='files', type=FileStorage, required=True, help='The file to upload')
|
||||
upload_parser.add_argument('name', location='form', type=str, required=False, help='Name of the document')
|
||||
upload_parser.add_argument('language', location='form', type=str, required=True, help='Language of the document')
|
||||
@@ -75,6 +76,7 @@ class AddDocument(Resource):
|
||||
validate_file_type(extension)
|
||||
|
||||
api_input = {
|
||||
'catalog_id': args.get('catalog_id'),
|
||||
'name': args.get('name') or filename,
|
||||
'language': args.get('language'),
|
||||
'user_context': args.get('user_context'),
|
||||
@@ -102,6 +104,7 @@ class AddDocument(Resource):
|
||||
|
||||
# Models for AddURL
|
||||
add_url_model = document_ns.model('AddURL', {
|
||||
'catalog_id': fields.Integer(required='True', description='ID of the catalog the URL needs to be added to'),
|
||||
'url': fields.String(required=True, description='URL of the document to add'),
|
||||
'name': fields.String(required=False, description='Name of the document'),
|
||||
'language': fields.String(required=True, description='Language of the document'),
|
||||
@@ -138,6 +141,7 @@ class AddURL(Resource):
|
||||
file_content, filename, extension = process_url(args['url'], tenant_id)
|
||||
|
||||
api_input = {
|
||||
'catalog_id': args['catalog_id'],
|
||||
'url': args['url'],
|
||||
'name': args.get('name') or filename,
|
||||
'language': args['language'],
|
||||
@@ -213,7 +217,8 @@ class DocumentResource(Resource):
|
||||
@document_ns.response(200, 'Document refreshed successfully')
|
||||
def post(self, document_id):
|
||||
"""Refresh a document"""
|
||||
new_version, result = refresh_document(document_id)
|
||||
tenant_id = get_jwt_identity()
|
||||
new_version, result = refresh_document(document_id, tenant_id)
|
||||
if new_version:
|
||||
return {'message': f'Document refreshed. New version: {new_version.id}. Task ID: {result}'}, 200
|
||||
else:
|
||||
|
||||
@@ -46,7 +46,7 @@ def check_database():
|
||||
def check_celery():
|
||||
try:
|
||||
# Send a simple task to Celery
|
||||
result = current_celery.send_task('tasks.ping', queue='embeddings')
|
||||
result = current_celery.send_task('ping', queue='eveai_workers.ping')
|
||||
response = result.get(timeout=10) # Wait for up to 10 seconds for a response
|
||||
return response == 'pong'
|
||||
except CeleryTimeoutError:
|
||||
|
||||
@@ -136,6 +136,8 @@ def register_blueprints(app):
|
||||
app.register_blueprint(interaction_bp)
|
||||
from .views.entitlements_views import entitlements_bp
|
||||
app.register_blueprint(entitlements_bp)
|
||||
from .views.administration_views import administration_bp
|
||||
app.register_blueprint(administration_bp)
|
||||
from .views.healthz_views import healthz_bp, init_healtz
|
||||
app.register_blueprint(healthz_bp)
|
||||
init_healtz(app)
|
||||
|
||||
File diff suppressed because one or more lines are too long
22
eveai_app/templates/administration/trigger_actions.html
Normal file
22
eveai_app/templates/administration/trigger_actions.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_selectable_table, render_pagination, render_field %}
|
||||
{% block title %}Trigger Actions{% endblock %}
|
||||
{% block content_title %}Trigger Actions{% endblock %}
|
||||
{% block content_description %}Manually trigger batch actions{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<!-- Trigger action Form -->
|
||||
<form method="POST" action="{{ url_for('administration_bp.handle_trigger_action') }}">
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="update_usages" class="btn btn-secondary">Update Usages</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
23
eveai_app/templates/document/catalog.html
Normal file
23
eveai_app/templates/document/catalog.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Catalog Registration{% endblock %}
|
||||
|
||||
{% block content_title %}Register Catalog{% endblock %}
|
||||
{% block content_description %}Define a new catalog of documents in Evie's Library{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Register Catalog</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
24
eveai_app/templates/document/catalogs.html
Normal file
24
eveai_app/templates/document/catalogs.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}Documents{% endblock %}
|
||||
|
||||
{% block content_title %}Catalogs{% endblock %}
|
||||
{% block content_description %}View Catalogs for Tenant{% endblock %}
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_catalog_selection') }}">
|
||||
{{ render_selectable_table(headers=["Catalog ID", "Name"], rows=rows, selectable=True, id="catalogsTable") }}
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="set_session_catalog" class="btn btn-primary">Set Session Catalog</button>
|
||||
<button type="submit" name="action" value="edit_catalog" class="btn btn-primary">Edit Catalog</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'document_bp.catalogs') }}
|
||||
{% endblock %}
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}">
|
||||
{{ render_selectable_table(headers=["ID", "URL", "File Loc.", "File Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
||||
{{ render_selectable_table(headers=["ID", "URL", "Object Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary">Edit Document Version</button>
|
||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger">Process Document Version</button>
|
||||
|
||||
@@ -23,15 +23,23 @@
|
||||
|
||||
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
|
||||
|
||||
<!-- Document Versions Table -->
|
||||
{{ render_selectable_sortable_table(
|
||||
headers=["ID", "File Type", "Processing", "Processing Start", "Processing Finish", "Processing Error"],
|
||||
rows=rows,
|
||||
selectable=True,
|
||||
id="documentVersionsTable",
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order
|
||||
) }}
|
||||
<div class="form-group mt-3">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}">
|
||||
<!-- Document Versions Table -->
|
||||
{{ render_selectable_sortable_table(
|
||||
headers=["ID", "File Type", "Processing", "Processing Start", "Processing Finish", "Processing Error"],
|
||||
rows=rows,
|
||||
selectable=True,
|
||||
id="documentVersionsTable",
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order
|
||||
) }}
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary">Edit Document Version</button>
|
||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger">Process Document Version</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||
{% 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 %}Documents{% endblock %}
|
||||
|
||||
@@ -8,18 +8,88 @@
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_document_selection') }}">
|
||||
{{ render_selectable_table(headers=["Document ID", "Name", "Valid From", "Valid To"], rows=rows, selectable=True, id="documentsTable") }}
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="edit_document" class="btn btn-primary">Edit Document</button>
|
||||
<button type="submit" name="action" value="document_versions" class="btn btn-secondary">Show Document Versions</button>
|
||||
<button type="submit" name="action" value="refresh_document" class="btn btn-secondary">Refresh Document (new version)</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Filter Form -->
|
||||
{% set filter_form %}
|
||||
<form method="GET" action="{{ url_for('document_bp.documents') }}">
|
||||
{{ render_filter_field('catalog_id', 'Catalog', filter_options['catalog_id'], filters.get('catalog_id', [])) }}
|
||||
{{ render_filter_field('validity', 'Validity', filter_options['validity'], filters.get('validity', [])) }}
|
||||
|
||||
<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_document_selection') }}">
|
||||
<!-- Documents Table -->
|
||||
{{ render_selectable_sortable_table_with_dict_headers(
|
||||
headers=[
|
||||
{"text": "ID", "sort": "id"},
|
||||
{"text": "Name", "sort": "name"},
|
||||
{"text": "Catalog", "sort": "catalog_name"},
|
||||
{"text": "Valid From", "sort": "valid_from"},
|
||||
{"text": "Valid To", "sort": "valid_to"}
|
||||
],
|
||||
rows=rows,
|
||||
selectable=True,
|
||||
id="documentsTable",
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order
|
||||
) }}
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" name="action" value="edit_document" class="btn btn-primary">Edit Document</button>
|
||||
<button type="submit" name="action" value="document_versions" class="btn btn-secondary">Show Document Versions</button>
|
||||
<button type="submit" name="action" value="refresh_document" class="btn btn-secondary">Refresh Document (new version)</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'document_bp.documents') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const table = document.getElementById('documentsTable');
|
||||
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;
|
||||
console.log('Selected Document ID:', documentId);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
25
eveai_app/templates/document/edit_catalog.html
Normal file
25
eveai_app/templates/document/edit_catalog.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Edit Catalog{% endblock %}
|
||||
|
||||
{% block content_title %}Edit Catalog{% endblock %}
|
||||
{% block content_description %}Edit a catalog of documents in Evie's Library.
|
||||
When you change chunking of embedding information, you'll need to manually refresh the library if you want immediate impact.
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Register Catalog</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -8,11 +8,17 @@
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
|
||||
{{ render_field(form.name, disabled_fields, exclude_fields) }}
|
||||
{{ render_field(form.valid_from, disabled_fields, exclude_fields) }}
|
||||
{{ render_field(form.valid_to, disabled_fields, exclude_fields) }}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="catalog_name">Catalog</label>
|
||||
<input type="text" class="form-control" id="catalog_name" value="{{ catalog_name }}" readonly>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Update Document</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
28
eveai_app/templates/entitlements/view_usages.html
Normal file
28
eveai_app/templates/entitlements/view_usages.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}View License Usage{% endblock %}
|
||||
|
||||
{% block content_title %}View License Usage{% endblock %}
|
||||
{% block content_description %}View License Usage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for('user_bp.handle_user_action') }}" method="POST">
|
||||
{{ render_selectable_table(headers=["Usage ID", "Start Date", "End Date", "Storage (MiB)", "Embedding (MiB)", "Interaction (tokens)"], rows=rows, selectable=False, id="usagesTable") }}
|
||||
<!-- <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="resend_confirmation_email" class="btn btn-secondary">Resend Confirmation Email</button>-->
|
||||
<!-- <button type="submit" name="action" value="send_password_reset_email" class="btn btn-secondary">Send Password Reset Email</button>-->
|
||||
<!-- <button type="submit" name="action" value="reset_uniquifier" class="btn btn-secondary">Reset Uniquifier</button>-->
|
||||
<!-- <!– Additional buttons can be added here for other actions –>-->
|
||||
<!-- </div>-->
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
<header class="header-2">
|
||||
<div class="page-header min-vh-25" style="background-image: url({{url_for('static', filename='/assets/img/EveAI_bg.jpg')}})" loading="lazy">
|
||||
<div class="page-header min-vh-25" style="background-image: url({{url_for('static', filename='/assets/img/EveAI_bg.jpg')}}); background-position: top left; background-repeat: no-repeat; background-size: cover;" loading="lazy">
|
||||
<span class="mask bg-gradient-primary opacity-4"></span>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
{% if embedding.url %}
|
||||
<a href="{{ embedding.url }}" target="_blank">{{ embedding.url }}</a>
|
||||
{% else %}
|
||||
{{ embedding.file_name }}
|
||||
{{ embedding.object_name }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -177,6 +177,48 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_selectable_sortable_table_with_dict_headers(headers, rows, selectable, id, sort_by, sort_order) %}
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-items-center mb-0" id="{{ id }}">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if selectable %}
|
||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Select</th>
|
||||
{% endif %}
|
||||
{% for header in headers %}
|
||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 sortable" data-sort="{{ header['sort'] }}">
|
||||
{{ header['text'] }}
|
||||
{% if sort_by == header['sort'] %}
|
||||
{% if sort_order == 'asc' %}
|
||||
<i class="fas fa-sort-up"></i>
|
||||
{% elif sort_order == 'desc' %}
|
||||
<i class="fas fa-sort-down"></i>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i class="fas fa-sort"></i>
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
{% if selectable %}
|
||||
<td><input type="radio" name="selected_row" value="{{ row[0].value }}"></td>
|
||||
{% endif %}
|
||||
{% for cell in row %}
|
||||
<td>{{ cell.value }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_accordion(accordion_id, accordion_items, header_title, header_description) %}
|
||||
<div class="accordion-1">
|
||||
<div class="container">
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ dropdown('Document Mgmt', 'note_stack', [
|
||||
{'name': 'Add Catalog', 'url': '/document/catalog', 'roles': ['Super User', 'Tenant Admin']},
|
||||
{'name': 'All Catalogs', 'url': '/document/catalogs', 'roles': ['Super User', 'Tenant Admin']},
|
||||
{'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Tenant Admin']},
|
||||
{'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Tenant Admin']},
|
||||
{'name': 'Add a list of URLs', 'url': '/document/add_urls', 'roles': ['Super User', 'Tenant Admin']},
|
||||
@@ -98,6 +100,8 @@
|
||||
{{ dropdown('Administration', 'settings', [
|
||||
{'name': 'License Tier Registration', 'url': '/entitlements/license_tier', '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': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']},
|
||||
]) }}
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -112,6 +116,17 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="navbar-nav d-lg-block d-none">
|
||||
<li class="nav-item">
|
||||
<a href="/document/catalogs" class="btn btn-sm bg-gradient-primary mb-0 me-2">
|
||||
{% if 'catalog_name' in session %}
|
||||
CATALOG: {{ session['catalog_name'] }}
|
||||
{% else %}
|
||||
CHOOSE CATALOG
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav d-lg-block d-none">
|
||||
<li class="nav-item">
|
||||
<a href="/session_defaults" class="btn btn-sm bg-gradient-primary mb-0">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<!-- Main Tenant Information -->
|
||||
{% set main_fields = ['name', 'website', 'default_language', 'allowed_languages', 'rag_context', 'type'] %}
|
||||
{% set main_fields = ['name', 'website', 'default_language', 'allowed_languages', 'timezone','rag_context', 'type'] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=[], include_fields=main_fields) }}
|
||||
{% endfor %}
|
||||
@@ -30,21 +30,6 @@
|
||||
License Information
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#chunking-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
Chunking
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-search-tab" role="tab" aria-controls="html-chunking" aria-selected="false">
|
||||
Embedding Search
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#tuning-tab" role="tab" aria-controls="html-chunking" aria-selected="false">
|
||||
Tuning
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content tab-space">
|
||||
@@ -62,8 +47,10 @@
|
||||
{{ render_included_field(field, disabled_fields=[], include_fields=license_fields) }}
|
||||
{% endfor %}
|
||||
<!-- Register API Key Button -->
|
||||
<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 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>
|
||||
@@ -78,27 +65,6 @@
|
||||
<p id="copy-message" style="display:none;color:green;">API key copied to clipboard</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chunking Settings Tab -->
|
||||
<div class="tab-pane fade" id="chunking-tab" role="tabpanel">
|
||||
{% set html_fields = ['html_tags', 'html_end_tags', 'html_included_elements', 'html_excluded_elements', 'html_excluded_classes', 'min_chunk_size', 'max_chunk_size'] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=[], include_fields=html_fields) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Embedding Search Settings Tab -->
|
||||
<div class="tab-pane fade" id="embedding-search-tab" role="tabpanel">
|
||||
{% set es_fields = ['es_k', 'es_similarity_threshold', ] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=[], include_fields=es_fields) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Tuning Settings Tab -->
|
||||
<div class="tab-pane fade" id="tuning-tab" role="tabpanel">
|
||||
{% set tuning_fields = ['embed_tuning', 'rag_tuning', ] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=[], include_fields=tuning_fields) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,21 +30,6 @@
|
||||
License Information
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#chunking-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||
Chunking
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-search-tab" role="tab" aria-controls="html-chunking" aria-selected="false">
|
||||
Embedding Search
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#tuning-tab" role="tab" aria-controls="html-chunking" aria-selected="false">
|
||||
Tuning
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content tab-space">
|
||||
@@ -78,27 +63,6 @@
|
||||
<p id="copy-message" style="display:none;color:green;">API key copied to clipboard</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chunking Settings Tab -->
|
||||
<div class="tab-pane fade" id="chunking-tab" role="tabpanel">
|
||||
{% set html_fields = ['html_tags', 'html_end_tags', 'html_included_elements', 'html_excluded_elements', 'html_excluded_classes', 'min_chunk_size', 'max_chunk_size'] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=html_fields, include_fields=html_fields) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Embedding Search Settings Tab -->
|
||||
<div class="tab-pane fade" id="embedding-search-tab" role="tabpanel">
|
||||
{% set es_fields = ['es_k', 'es_similarity_threshold', ] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=es_fields, include_fields=es_fields) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Tuning Settings Tab -->
|
||||
<div class="tab-pane fade" id="tuning-tab" role="tabpanel">
|
||||
{% set tuning_fields = ['embed_tuning', 'rag_tuning', ] %}
|
||||
{% for field in form %}
|
||||
{{ render_included_field(field, disabled_fields=tuning_fields, include_fields=tuning_fields) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
7
eveai_app/views/administration_forms.py
Normal file
7
eveai_app/views/administration_forms.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from flask import current_app
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields.simple import SubmitField
|
||||
|
||||
|
||||
class TriggerActionForm(FlaskForm):
|
||||
submit = SubmitField('Submit')
|
||||
39
eveai_app/views/administration_views.py
Normal file
39
eveai_app/views/administration_views.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import uuid
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
|
||||
from flask_security import hash_password, roles_required, roles_accepted, current_user
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from .administration_forms import TriggerActionForm
|
||||
|
||||
administration_bp = Blueprint('administration_bp', __name__, url_prefix='/administration')
|
||||
|
||||
|
||||
@administration_bp.route('/trigger_actions', methods=['GET'])
|
||||
@roles_accepted('Super User')
|
||||
def trigger_actions():
|
||||
form = TriggerActionForm()
|
||||
return render_template('administration/trigger_actions.html', form=form)
|
||||
|
||||
|
||||
@administration_bp.route('/handle_trigger_action', methods=['POST'])
|
||||
@roles_accepted('Super User')
|
||||
def handle_trigger_action():
|
||||
action = request.form['action']
|
||||
match action:
|
||||
case 'update_usages':
|
||||
try:
|
||||
# Use send_task to trigger the task since it's part of another component (eveai_entitlements)
|
||||
task = current_celery.send_task('update_usages', queue='entitlements')
|
||||
|
||||
current_app.logger.info(f"Usage update task triggered: {task.id}")
|
||||
flash('Usage update task has been triggered successfully!', 'success')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to trigger usage update task: {str(e)}")
|
||||
flash(f'Failed to trigger usage update: {str(e)}', 'danger')
|
||||
|
||||
return redirect(prefixed_url_for('administration_bp.trigger_actions'))
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import session, current_app
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (StringField, BooleanField, SubmitField, DateField,
|
||||
from wtforms import (StringField, BooleanField, SubmitField, DateField, IntegerField, FloatField, SelectMultipleField,
|
||||
SelectField, FieldList, FormField, TextAreaField, URLField)
|
||||
from wtforms.validators import DataRequired, Length, Optional, URL, ValidationError
|
||||
from wtforms.validators import DataRequired, Length, Optional, URL, ValidationError, NumberRange
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
import json
|
||||
|
||||
@@ -23,6 +23,36 @@ def validate_json(form, field):
|
||||
raise ValidationError('Invalid JSON format')
|
||||
|
||||
|
||||
class CatalogForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
# HTML Embedding Variables
|
||||
html_tags = StringField('HTML Tags', validators=[DataRequired()],
|
||||
default='p, h1, h2, h3, h4, h5, h6, li, , tbody, tr, td')
|
||||
html_end_tags = StringField('HTML End Tags', validators=[DataRequired()],
|
||||
default='p, li')
|
||||
html_included_elements = StringField('HTML Included Elements', validators=[Optional()])
|
||||
html_excluded_elements = StringField('HTML Excluded Elements', validators=[Optional()])
|
||||
html_excluded_classes = StringField('HTML Excluded Classes', validators=[Optional()])
|
||||
min_chunk_size = IntegerField('Minimum Chunk Size (2000)', validators=[NumberRange(min=0), Optional()],
|
||||
default=2000)
|
||||
max_chunk_size = IntegerField('Maximum Chunk Size (3000)', validators=[NumberRange(min=0), Optional()],
|
||||
default=3000)
|
||||
# Embedding Search variables
|
||||
es_k = IntegerField('Limit for Searching Embeddings (5)',
|
||||
default=5,
|
||||
validators=[NumberRange(min=0)])
|
||||
es_similarity_threshold = FloatField('Similarity Threshold for Searching Embeddings (0.5)',
|
||||
default=0.5,
|
||||
validators=[NumberRange(min=0, max=1)])
|
||||
# Chat Variables
|
||||
chat_RAG_temperature = FloatField('RAG Temperature', default=0.3, validators=[NumberRange(min=0, max=1)])
|
||||
chat_no_RAG_temperature = FloatField('No RAG Temperature', default=0.5, validators=[NumberRange(min=0, max=1)])
|
||||
# Tuning variables
|
||||
embed_tuning = BooleanField('Enable Embedding Tuning', default=False)
|
||||
rag_tuning = BooleanField('Enable RAG Tuning', default=False)
|
||||
|
||||
|
||||
class AddDocumentForm(FlaskForm):
|
||||
file = FileField('File', validators=[FileRequired(), allowed_file])
|
||||
name = StringField('Name', validators=[Length(max=100)])
|
||||
|
||||
102
eveai_app/views/document_list_view.py
Normal file
102
eveai_app/views/document_list_view.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from datetime import datetime
|
||||
from flask import request, render_template, session
|
||||
from sqlalchemy import desc, asc, or_, and_, cast, Integer
|
||||
from common.models.document import Document, Catalog
|
||||
from common.utils.filtered_list_view import FilteredListView
|
||||
from common.utils.view_assistants import prepare_table_for_macro
|
||||
|
||||
|
||||
class DocumentListView(FilteredListView):
|
||||
allowed_filters = ['catalog_id', 'validity']
|
||||
allowed_sorts = ['id', 'name', 'catalog_name', 'valid_from', 'valid_to']
|
||||
|
||||
def get_query(self):
|
||||
return Document.query.join(Catalog).add_columns(
|
||||
Document.id,
|
||||
Document.name,
|
||||
Catalog.name.label('catalog_name'),
|
||||
Document.valid_from,
|
||||
Document.valid_to
|
||||
)
|
||||
|
||||
def apply_filters(self, query):
|
||||
filters = request.args.to_dict(flat=False)
|
||||
|
||||
if 'catalog_id' in filters:
|
||||
catalog_ids = filters['catalog_id']
|
||||
if catalog_ids:
|
||||
# Convert catalog_ids to a list of integers
|
||||
catalog_ids = [int(cid) for cid in catalog_ids if cid.isdigit()]
|
||||
if catalog_ids:
|
||||
query = query.filter(Document.catalog_id.in_(catalog_ids))
|
||||
|
||||
if 'validity' in filters:
|
||||
now = datetime.utcnow().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)
|
||||
)
|
||||
)
|
||||
|
||||
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:
|
||||
if sort_by == 'catalog_name':
|
||||
column = Catalog.name
|
||||
else:
|
||||
column = getattr(Document, 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, datetime):
|
||||
return date.strftime('%Y-%m-%d')
|
||||
elif isinstance(date, str):
|
||||
return date
|
||||
else:
|
||||
return ''
|
||||
|
||||
rows = [
|
||||
[
|
||||
{'value': item.id, 'class': '', 'type': 'text'},
|
||||
{'value': item.name, 'class': '', 'type': 'text'},
|
||||
{'value': item.catalog_name, 'class': '', 'type': 'text'},
|
||||
{'value': format_date(item.valid_from), 'class': '', 'type': 'text'},
|
||||
{'value': format_date(item.valid_to), 'class': '', 'type': 'text'}
|
||||
] for item in pagination.items
|
||||
]
|
||||
|
||||
catalogs = Catalog.query.all()
|
||||
|
||||
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(catalogs)
|
||||
}
|
||||
return render_template(self.template, **context)
|
||||
|
||||
def get_filter_options(self, catalogs):
|
||||
return {
|
||||
'catalog_id': [(str(cat.id), cat.name) for cat in catalogs],
|
||||
'validity': [('valid', 'Valid'), ('all', 'All')]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class DocumentVersionListView(FilteredListView):
|
||||
allowed_sorts = ['id', 'processing_started_at', 'processing_finished_at', 'processing_error']
|
||||
|
||||
def get_query(self):
|
||||
return DocumentVersion.query.join(Document).filter(Document.tenant_id == session.get('tenant', {}).get('id'))
|
||||
return DocumentVersion.query.join(Document)
|
||||
|
||||
def apply_filters(self, query):
|
||||
filters = request.args.to_dict()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ast
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
|
||||
from babel.messages.setuptools_frontend import update_catalog
|
||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
||||
from flask_security import roles_accepted, current_user
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import aliased
|
||||
from werkzeug.utils import secure_filename
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import requests
|
||||
@@ -12,18 +14,20 @@ from urllib.parse import urlparse, unquote
|
||||
import io
|
||||
import json
|
||||
|
||||
from common.models.document import Document, DocumentVersion
|
||||
from common.models.document import Document, DocumentVersion, Catalog
|
||||
from common.extensions import db, minio_client
|
||||
from common.utils.document_utils import validate_file_type, create_document_stack, start_embedding_task, process_url, \
|
||||
process_multiple_urls, get_documents_list, edit_document, \
|
||||
edit_document_version, refresh_document
|
||||
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \
|
||||
EveAIDoubleURLException
|
||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, AddURLsForm
|
||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, AddURLsForm, \
|
||||
CatalogForm
|
||||
from common.utils.middleware import mw_before_request
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro, form_to_dict
|
||||
from .document_list_view import DocumentListView
|
||||
from .document_version_list_view import DocumentVersionListView
|
||||
|
||||
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
|
||||
@@ -52,6 +56,123 @@ def before_request():
|
||||
raise
|
||||
|
||||
|
||||
@document_bp.route('/catalog', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def catalog():
|
||||
form = CatalogForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
new_catalog = Catalog()
|
||||
form.populate_obj(new_catalog)
|
||||
# Handle Embedding Variables
|
||||
new_catalog.html_tags = [tag.strip() for tag in form.html_tags.data.split(',')] if form.html_tags.data else []
|
||||
new_catalog.html_end_tags = [tag.strip() for tag in form.html_end_tags.data.split(',')] \
|
||||
if form.html_end_tags.data else []
|
||||
new_catalog.html_included_elements = [tag.strip() for tag in form.html_included_elements.data.split(',')] \
|
||||
if form.html_included_elements.data else []
|
||||
new_catalog.html_excluded_elements = [tag.strip() for tag in form.html_excluded_elements.data.split(',')] \
|
||||
if form.html_excluded_elements.data else []
|
||||
new_catalog.html_excluded_classes = [cls.strip() for cls in form.html_excluded_classes.data.split(',')] \
|
||||
if form.html_excluded_classes.data else []
|
||||
set_logging_information(new_catalog, dt.now(tz.utc))
|
||||
|
||||
try:
|
||||
db.session.add(new_catalog)
|
||||
db.session.commit()
|
||||
flash('Catalog successfully added!', 'success')
|
||||
current_app.logger.info(f'Catalog {new_catalog.name} successfully added for tenant {tenant_id}!')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to add catalog. Error: {e}', 'danger')
|
||||
current_app.logger.error(f'Failed to add catalog {new_catalog.name}'
|
||||
f'for tenant {tenant_id}. Error: {str(e)}')
|
||||
|
||||
return render_template('document/catalog.html', form=form)
|
||||
|
||||
|
||||
@document_bp.route('/catalogs', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def catalogs():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
query = Catalog.query.order_by(Catalog.id)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
the_catalogs = pagination.items
|
||||
|
||||
# prepare table data
|
||||
rows = prepare_table_for_macro(the_catalogs, [('id', ''), ('name', '')])
|
||||
|
||||
# Render the catalogs in a template
|
||||
return render_template('document/catalogs.html', rows=rows, pagination=pagination)
|
||||
|
||||
|
||||
@document_bp.route('/handle_catalog_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def handle_catalog_selection():
|
||||
catalog_identification = request.form.get('selected_row')
|
||||
catalog_id = ast.literal_eval(catalog_identification).get('value')
|
||||
action = request.form['action']
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
|
||||
if action == 'set_session_catalog':
|
||||
current_app.logger.info(f'Setting session catalog to {catalog.name}')
|
||||
session['catalog_id'] = catalog_id
|
||||
session['catalog_name'] = catalog.name
|
||||
current_app.logger.info(f'Finished setting session catalog to {catalog.name}')
|
||||
elif action == 'edit_catalog':
|
||||
return redirect(prefixed_url_for('document_bp.edit_catalog', catalog_id=catalog_id))
|
||||
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
|
||||
|
||||
@document_bp.route('/catalog/<int:catalog_id>', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def edit_catalog(catalog_id):
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
form = CatalogForm(obj=catalog)
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
|
||||
# Convert arrays to comma-separated strings for display
|
||||
if request.method == 'GET':
|
||||
form.html_tags.data = ', '.join(catalog.html_tags or '')
|
||||
form.html_end_tags.data = ', '.join(catalog.html_end_tags or '')
|
||||
form.html_included_elements.data = ', '.join(catalog.html_included_elements or '')
|
||||
form.html_excluded_elements.data = ', '.join(catalog.html_excluded_elements or '')
|
||||
form.html_excluded_classes.data = ', '.join(catalog.html_excluded_classes or '')
|
||||
|
||||
if request.method == 'POST' and form.validate_on_submit():
|
||||
form.populate_obj(catalog)
|
||||
# Handle Embedding Variables
|
||||
catalog.html_tags = [tag.strip() for tag in form.html_tags.data.split(',')] if form.html_tags.data else []
|
||||
catalog.html_end_tags = [tag.strip() for tag in form.html_end_tags.data.split(',')] \
|
||||
if form.html_end_tags.data else []
|
||||
catalog.html_included_elements = [tag.strip() for tag in form.html_included_elements.data.split(',')] \
|
||||
if form.html_included_elements.data else []
|
||||
catalog.html_excluded_elements = [tag.strip() for tag in form.html_excluded_elements.data.split(',')] \
|
||||
if form.html_excluded_elements.data else []
|
||||
catalog.html_excluded_classes = [cls.strip() for cls in form.html_excluded_classes.data.split(',')] \
|
||||
if form.html_excluded_classes.data else []
|
||||
update_logging_information(catalog, dt.now(tz.utc))
|
||||
try:
|
||||
db.session.add(catalog)
|
||||
db.session.commit()
|
||||
flash('Catalog successfully updated successfully!', 'success')
|
||||
current_app.logger.info(f'Catalog {catalog.name} successfully updated for tenant {tenant_id}')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update catalog. Error: {e}', 'danger')
|
||||
current_app.logger.error(f'Failed to update catalog {catalog_id} for tenant {tenant_id}. Error: {str(e)}')
|
||||
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
|
||||
return render_template('document/edit_catalog.html', form=form, catalog_id=catalog_id)
|
||||
|
||||
|
||||
@document_bp.route('/add_document', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def add_document():
|
||||
@@ -60,6 +181,7 @@ def add_document():
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
tenant_id = session['tenant']['id']
|
||||
catalog_id = session['catalog_id']
|
||||
file = form.file.data
|
||||
filename = secure_filename(file.filename)
|
||||
extension = filename.rsplit('.', 1)[1].lower()
|
||||
@@ -68,6 +190,7 @@ def add_document():
|
||||
|
||||
current_app.logger.debug(f'Language on form: {form.language.data}')
|
||||
api_input = {
|
||||
'catalog_id': catalog_id,
|
||||
'name': form.name.data,
|
||||
'language': form.language.data,
|
||||
'user_context': form.user_context.data,
|
||||
@@ -100,11 +223,13 @@ def add_url():
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
tenant_id = session['tenant']['id']
|
||||
catalog_id = session['catalog_id']
|
||||
url = form.url.data
|
||||
|
||||
file_content, filename, extension = process_url(url, tenant_id)
|
||||
|
||||
api_input = {
|
||||
'catalog_id': catalog_id,
|
||||
'name': form.name.data or filename,
|
||||
'url': url,
|
||||
'language': form.language.data,
|
||||
@@ -171,22 +296,23 @@ def add_urls():
|
||||
@document_bp.route('/documents', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def documents():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
pagination = get_documents_list(page, per_page)
|
||||
docs = pagination.items
|
||||
|
||||
rows = prepare_table_for_macro(docs, [('id', ''), ('name', ''), ('valid_from', ''), ('valid_to', '')])
|
||||
|
||||
return render_template('document/documents.html', rows=rows, pagination=pagination)
|
||||
view = DocumentListView(Document, 'document/documents.html', per_page=10)
|
||||
return view.get()
|
||||
|
||||
|
||||
@document_bp.route('/handle_document_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def handle_document_selection():
|
||||
document_identification = request.form['selected_row']
|
||||
doc_id = ast.literal_eval(document_identification).get('value')
|
||||
if isinstance(document_identification, int) or document_identification.isdigit():
|
||||
doc_id = int(document_identification)
|
||||
else:
|
||||
# If it's not an integer, assume it's a string representation of a dictionary
|
||||
try:
|
||||
doc_id = ast.literal_eval(document_identification).get('value')
|
||||
except (ValueError, AttributeError):
|
||||
flash('Invalid document selection.', 'error')
|
||||
return redirect(prefixed_url_for('document_bp.documents'))
|
||||
|
||||
action = request.form['action']
|
||||
|
||||
@@ -208,9 +334,25 @@ def handle_document_selection():
|
||||
@document_bp.route('/edit_document/<int:document_id>', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def edit_document_view(document_id):
|
||||
doc = Document.query.get_or_404(document_id)
|
||||
# Use an alias for the Catalog to avoid column name conflicts
|
||||
CatalogAlias = aliased(Catalog)
|
||||
|
||||
# Query for the document and its catalog
|
||||
result = db.session.query(Document, CatalogAlias.name.label('catalog_name')) \
|
||||
.join(CatalogAlias, Document.catalog_id == CatalogAlias.id) \
|
||||
.filter(Document.id == document_id) \
|
||||
.first_or_404()
|
||||
|
||||
doc, catalog_name = result
|
||||
|
||||
form = EditDocumentForm(obj=doc)
|
||||
|
||||
if request.method == 'GET':
|
||||
# Populate form with current values
|
||||
form.name.data = doc.name
|
||||
form.valid_from.data = doc.valid_from
|
||||
form.valid_to.data = doc.valid_to
|
||||
|
||||
if form.validate_on_submit():
|
||||
updated_doc, error = edit_document(
|
||||
document_id,
|
||||
@@ -226,7 +368,7 @@ def edit_document_view(document_id):
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
|
||||
return render_template('document/edit_document.html', form=form, document_id=document_id)
|
||||
return render_template('document/edit_document.html', form=form, document_id=document_id, catalog_name=catalog_name)
|
||||
|
||||
|
||||
@document_bp.route('/edit_document_version/<int:document_version_id>', methods=['GET', 'POST'])
|
||||
@@ -268,8 +410,8 @@ def document_versions(document_id):
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
doc_langs = pagination.items
|
||||
|
||||
rows = prepare_table_for_macro(doc_langs, [('id', ''), ('url', ''), ('file_location', ''),
|
||||
('file_name', ''), ('file_type', ''),
|
||||
rows = prepare_table_for_macro(doc_langs, [('id', ''), ('url', ''),
|
||||
('object_name', ''), ('file_type', ''),
|
||||
('processing', ''), ('processing_started_at', ''),
|
||||
('processing_finished_at', ''), ('processing_error', '')])
|
||||
|
||||
@@ -280,7 +422,15 @@ def document_versions(document_id):
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def handle_document_version_selection():
|
||||
document_version_identification = request.form['selected_row']
|
||||
doc_vers_id = ast.literal_eval(document_version_identification).get('value')
|
||||
if isinstance(document_version_identification, int) or document_version_identification.isdigit():
|
||||
doc_vers_id = int(document_version_identification)
|
||||
else:
|
||||
# If it's not an integer, assume it's a string representation of a dictionary
|
||||
try:
|
||||
doc_vers_id = ast.literal_eval(document_version_identification).get('value')
|
||||
except (ValueError, AttributeError):
|
||||
flash('Invalid document version selection.', 'error')
|
||||
return redirect(prefixed_url_for('document_bp.document_versions_list'))
|
||||
|
||||
action = request.form['action']
|
||||
|
||||
@@ -332,7 +482,7 @@ def refresh_all_documents():
|
||||
|
||||
|
||||
def refresh_document_view(document_id):
|
||||
new_version, result = refresh_document(document_id)
|
||||
new_version, result = refresh_document(document_id, session['tenant']['id'])
|
||||
if new_version:
|
||||
flash(f'Document refreshed. New version: {new_version.id}. Task ID: {result}', 'success')
|
||||
else:
|
||||
@@ -349,10 +499,9 @@ def re_embed_latest_versions():
|
||||
|
||||
|
||||
def process_version(version_id):
|
||||
task = current_celery.send_task('create_embeddings', queue='embeddings', args=[
|
||||
session['tenant']['id'],
|
||||
version_id,
|
||||
])
|
||||
task = current_celery.send_task('create_embeddings',
|
||||
args=[session['tenant']['id'], version_id,],
|
||||
queue='embeddings')
|
||||
current_app.logger.info(f'Embedding creation retriggered by user {current_user.id}, {current_user.email} '
|
||||
f'for tenant {session["tenant"]["id"]}, '
|
||||
f'Document Version {version_id}. '
|
||||
@@ -398,47 +547,47 @@ def fetch_html(url):
|
||||
return response.content
|
||||
|
||||
|
||||
def prepare_document_data(docs):
|
||||
rows = []
|
||||
for doc in docs:
|
||||
doc_row = [{'value': doc.name, 'class': '', 'type': 'text'},
|
||||
{'value': doc.created_at.strftime("%Y-%m-%d %H:%M:%S"), 'class': '', 'type': 'text'}]
|
||||
# Document basic details
|
||||
if doc.valid_from:
|
||||
doc_row.append({'value': doc.valid_from.strftime("%Y-%m-%d"), 'class': '', 'type': 'text'})
|
||||
else:
|
||||
doc_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
|
||||
# Nested languages and versions
|
||||
languages_rows = []
|
||||
for lang in doc.languages:
|
||||
lang_row = [{'value': lang.language, 'class': '', 'type': 'text'}]
|
||||
|
||||
# Latest version details if available (should be available ;-) )
|
||||
if lang.latest_version:
|
||||
lang_row.append({'value': lang.latest_version.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'class': '', 'type': 'text'})
|
||||
if lang.latest_version.url:
|
||||
lang_row.append({'value': lang.latest_version.url,
|
||||
'class': '', 'type': 'link', 'href': lang.latest_version.url})
|
||||
else:
|
||||
lang_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
|
||||
if lang.latest_version.file_name:
|
||||
lang_row.append({'value': lang.latest_version.file_name, 'class': '', 'type': 'text'})
|
||||
else:
|
||||
lang_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
|
||||
if lang.latest_version.file_type:
|
||||
lang_row.append({'value': lang.latest_version.file_type, 'class': '', 'type': 'text'})
|
||||
else:
|
||||
lang_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
# Include other details as necessary
|
||||
|
||||
languages_rows.append(lang_row)
|
||||
|
||||
doc_row.append({'is_group': True, 'colspan': '5',
|
||||
'headers': ['Language', 'Latest Version', 'URL', 'File Name', 'Type'],
|
||||
'sub_rows': languages_rows})
|
||||
rows.append(doc_row)
|
||||
return rows
|
||||
# def prepare_document_data(docs):
|
||||
# rows = []
|
||||
# for doc in docs:
|
||||
# doc_row = [{'value': doc.name, 'class': '', 'type': 'text'},
|
||||
# {'value': doc.created_at.strftime("%Y-%m-%d %H:%M:%S"), 'class': '', 'type': 'text'}]
|
||||
# # Document basic details
|
||||
# if doc.valid_from:
|
||||
# doc_row.append({'value': doc.valid_from.strftime("%Y-%m-%d"), 'class': '', 'type': 'text'})
|
||||
# else:
|
||||
# doc_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
#
|
||||
# # Nested languages and versions
|
||||
# languages_rows = []
|
||||
# for lang in doc.languages:
|
||||
# lang_row = [{'value': lang.language, 'class': '', 'type': 'text'}]
|
||||
#
|
||||
# # Latest version details if available (should be available ;-) )
|
||||
# if lang.latest_version:
|
||||
# lang_row.append({'value': lang.latest_version.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
# 'class': '', 'type': 'text'})
|
||||
# if lang.latest_version.url:
|
||||
# lang_row.append({'value': lang.latest_version.url,
|
||||
# 'class': '', 'type': 'link', 'href': lang.latest_version.url})
|
||||
# else:
|
||||
# lang_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
#
|
||||
# if lang.latest_version.object_name:
|
||||
# lang_row.append({'value': lang.latest_version.object_name, 'class': '', 'type': 'text'})
|
||||
# else:
|
||||
# lang_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
#
|
||||
# if lang.latest_version.file_type:
|
||||
# lang_row.append({'value': lang.latest_version.file_type, 'class': '', 'type': 'text'})
|
||||
# else:
|
||||
# lang_row.append({'value': '', 'class': '', 'type': 'text'})
|
||||
# # Include other details as necessary
|
||||
#
|
||||
# languages_rows.append(lang_row)
|
||||
#
|
||||
# doc_row.append({'is_group': True, 'colspan': '5',
|
||||
# 'headers': ['Language', 'Latest Version', 'URL', 'File Name', 'Type'],
|
||||
# 'sub_rows': languages_rows})
|
||||
# rows.append(doc_row)
|
||||
# return rows
|
||||
|
||||
@@ -2,18 +2,14 @@ import uuid
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
|
||||
from flask_security import hash_password, roles_required, roles_accepted, current_user
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import or_, desc
|
||||
import ast
|
||||
|
||||
from common.models.entitlements import License, LicenseTier, LicenseUsage, BusinessEventLog
|
||||
from common.extensions import db, security, minio_client, simple_encryption
|
||||
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||
from .entitlements_forms import LicenseTierForm, LicenseForm
|
||||
from common.utils.database import Database
|
||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||
from common.utils.simple_encryption import generate_api_key
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
|
||||
entitlements_bp = Blueprint('entitlements_bp', __name__, url_prefix='/entitlements')
|
||||
@@ -174,14 +170,14 @@ def create_license(license_tier_id):
|
||||
db.session.add(new_license)
|
||||
db.session.commit()
|
||||
flash('License created successfully', 'success')
|
||||
return redirect(prefixed_url_for('entitlements_bp/edit_license', license_id=new_license.id))
|
||||
return redirect(prefixed_url_for('entitlements_bp.edit_license', license_id=new_license.id))
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Error creating license: {str(e)}', 'error')
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
|
||||
return render_template('entitlements/license.html', form=form)
|
||||
return render_template('entitlements/license.html', form=form, ext_disabled_fields=[])
|
||||
|
||||
|
||||
@entitlements_bp.route('/license/<int:license_id>', methods=['GET', 'POST'])
|
||||
@@ -215,3 +211,25 @@ def edit_license(license_id):
|
||||
|
||||
return render_template('entitlements/license.html', form=form, license_tier_id=license_tier.id,
|
||||
ext_disabled_fields=disabled_fields)
|
||||
|
||||
|
||||
@entitlements_bp.route('/view_usages')
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def view_usages():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
query = LicenseUsage.query.filter_by(tenant_id=tenant_id).order_by(desc(LicenseUsage.id))
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
lus = pagination.items
|
||||
|
||||
# prepare table data
|
||||
|
||||
rows = prepare_table_for_macro(lus, [('id', ''), ('period_start_date', ''), ('period_end_date', ''),
|
||||
('storage_mb_used', ''), ('embedding_mb_used', ''),
|
||||
('interaction_total_tokens_used', '')])
|
||||
|
||||
# Render the users in a template
|
||||
return render_template('entitlements/view_usages.html', rows=rows, pagination=pagination)
|
||||
|
||||
@@ -93,17 +93,17 @@ def view_chat_session(chat_session_id):
|
||||
# Fetch all related embeddings for the interactions in this session
|
||||
embedding_query = (db.session.query(InteractionEmbedding.interaction_id,
|
||||
DocumentVersion.url,
|
||||
DocumentVersion.file_name)
|
||||
DocumentVersion.object_name)
|
||||
.join(Embedding, InteractionEmbedding.embedding_id == Embedding.id)
|
||||
.join(DocumentVersion, Embedding.doc_vers_id == DocumentVersion.id)
|
||||
.filter(InteractionEmbedding.interaction_id.in_([i.id for i in interactions])))
|
||||
|
||||
# Create a dictionary to store embeddings for each interaction
|
||||
embeddings_dict = {}
|
||||
for interaction_id, url, file_name in embedding_query:
|
||||
for interaction_id, url, object_name in embedding_query:
|
||||
if interaction_id not in embeddings_dict:
|
||||
embeddings_dict[interaction_id] = []
|
||||
embeddings_dict[interaction_id].append({'url': url, 'file_name': file_name})
|
||||
embeddings_dict[interaction_id].append({'url': url, 'object_name': object_name})
|
||||
|
||||
return render_template('interaction/view_chat_session.html',
|
||||
chat_session=chat_session,
|
||||
|
||||
@@ -48,22 +48,6 @@ def tenant():
|
||||
new_tenant = Tenant()
|
||||
form.populate_obj(new_tenant)
|
||||
|
||||
# Handle Embedding Variables
|
||||
new_tenant.html_tags = [tag.strip() for tag in form.html_tags.data.split(',')] if form.html_tags.data else []
|
||||
new_tenant.html_end_tags = [tag.strip() for tag in form.html_end_tags.data.split(',')] \
|
||||
if form.html_end_tags.data else []
|
||||
new_tenant.html_included_elements = [tag.strip() for tag in form.html_included_elements.data.split(',')] \
|
||||
if form.html_included_elements.data else []
|
||||
new_tenant.html_excluded_elements = [tag.strip() for tag in form.html_excluded_elements.data.split(',')] \
|
||||
if form.html_excluded_elements.data else []
|
||||
new_tenant.html_excluded_classes = [cls.strip() for cls in form.html_excluded_classes.data.split(',')] \
|
||||
if form.html_excluded_classes.data else []
|
||||
|
||||
current_app.logger.debug(f'html_tags: {new_tenant.html_tags},'
|
||||
f'html_end_tags: {new_tenant.html_end_tags},'
|
||||
f'html_included_elements: {new_tenant.html_included_elements},'
|
||||
f'html_excluded_elements: {new_tenant.html_excluded_elements}')
|
||||
|
||||
# Handle Timestamps
|
||||
timestamp = dt.now(tz.utc)
|
||||
new_tenant.created_at = timestamp
|
||||
@@ -105,30 +89,11 @@ def edit_tenant(tenant_id):
|
||||
if request.method == 'GET':
|
||||
# Populate the form with tenant data
|
||||
form.populate_obj(tenant)
|
||||
if tenant.html_tags:
|
||||
form.html_tags.data = ', '.join(tenant.html_tags)
|
||||
if tenant.html_end_tags:
|
||||
form.html_end_tags.data = ', '.join(tenant.html_end_tags)
|
||||
if tenant.html_included_elements:
|
||||
form.html_included_elements.data = ', '.join(tenant.html_included_elements)
|
||||
if tenant.html_excluded_elements:
|
||||
form.html_excluded_elements.data = ', '.join(tenant.html_excluded_elements)
|
||||
if tenant.html_excluded_classes:
|
||||
form.html_excluded_classes.data = ', '.join(tenant.html_excluded_classes)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_app.logger.debug(f'Updating tenant {tenant_id}')
|
||||
# Populate the tenant with form data
|
||||
form.populate_obj(tenant)
|
||||
# Then handle the special fields manually
|
||||
tenant.html_tags = [tag.strip() for tag in form.html_tags.data.split(',') if tag.strip()]
|
||||
tenant.html_end_tags = [tag.strip() for tag in form.html_end_tags.data.split(',') if tag.strip()]
|
||||
tenant.html_included_elements = [elem.strip() for elem in form.html_included_elements.data.split(',') if
|
||||
elem.strip()]
|
||||
tenant.html_excluded_elements = [elem.strip() for elem in form.html_excluded_elements.data.split(',') if
|
||||
elem.strip()]
|
||||
tenant.html_excluded_classes = [elem.strip() for elem in form.html_excluded_classes.data.split(',') if
|
||||
elem.strip()]
|
||||
|
||||
db.session.commit()
|
||||
flash('Tenant updated successfully.', 'success')
|
||||
@@ -266,10 +231,16 @@ def handle_tenant_selection():
|
||||
tenant_identification = request.form['selected_row']
|
||||
tenant_id = ast.literal_eval(tenant_identification).get('value')
|
||||
the_tenant = Tenant.query.get(tenant_id)
|
||||
|
||||
# set tenant information in the session
|
||||
session['tenant'] = the_tenant.to_dict()
|
||||
session['default_language'] = the_tenant.default_language
|
||||
session['embedding_model'] = the_tenant.embedding_model
|
||||
session['llm_model'] = the_tenant.llm_model
|
||||
# remove catalog-related items from the session
|
||||
session.pop('catalog_id', None)
|
||||
session.pop('catalog_name', None)
|
||||
|
||||
action = request.form['action']
|
||||
|
||||
match action:
|
||||
|
||||
44
eveai_beat/__init__.py
Normal file
44
eveai_beat/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import logging
|
||||
import logging.config
|
||||
from flask import Flask
|
||||
import os
|
||||
|
||||
from common.utils.celery_utils import make_celery, init_celery
|
||||
from config.logging_config import LOGGING
|
||||
from config.config import get_config
|
||||
|
||||
|
||||
def create_app(config_file=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
environment = os.getenv('FLASK_ENV', 'development')
|
||||
|
||||
match environment:
|
||||
case 'development':
|
||||
app.config.from_object(get_config('dev'))
|
||||
case 'production':
|
||||
app.config.from_object(get_config('prod'))
|
||||
case _:
|
||||
app.config.from_object(get_config('dev'))
|
||||
|
||||
logging.config.dictConfig(LOGGING)
|
||||
|
||||
register_extensions(app)
|
||||
|
||||
celery = make_celery(app.name, app.config)
|
||||
init_celery(celery, app, is_beat=True)
|
||||
|
||||
from . import schedule
|
||||
celery.conf.beat_schedule = schedule.beat_schedule
|
||||
|
||||
app.logger.info("EveAI Beat Scheduler Started Successfully")
|
||||
app.logger.info("-------------------------------------------------------------------------------------------------")
|
||||
|
||||
return app, celery
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
pass
|
||||
|
||||
|
||||
app, celery = create_app()
|
||||
17
eveai_beat/schedule.py
Normal file
17
eveai_beat/schedule.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from celery.schedules import crontab
|
||||
|
||||
# Define the Celery beat schedule here
|
||||
beat_schedule = {
|
||||
'update-tenant-usages-every-hour': {
|
||||
'task': 'update_usages',
|
||||
'schedule': crontab(minute='0'), # Runs every hour
|
||||
'args': (),
|
||||
'options': {'queue': 'entitlements'}
|
||||
},
|
||||
# 'send-invoices-every-month': {
|
||||
# 'task': 'send_invoices',
|
||||
# 'schedule': crontab(day_of_month=1, hour=0, minute=0), # Runs on the 1st of every month
|
||||
# 'args': ()
|
||||
# },
|
||||
# Add more schedules as needed
|
||||
}
|
||||
@@ -109,14 +109,16 @@ def handle_message(data):
|
||||
room = session.get('room')
|
||||
|
||||
# Offload actual processing of question
|
||||
task = current_celery.send_task('ask_question', queue='llm_interactions', args=[
|
||||
current_tenant_id,
|
||||
data['message'],
|
||||
data['language'],
|
||||
session['session_id'],
|
||||
data['timezone'],
|
||||
room
|
||||
])
|
||||
task = current_celery.send_task('ask_question',
|
||||
queue='llm_interactions',
|
||||
args=[
|
||||
current_tenant_id,
|
||||
data['message'],
|
||||
data['language'],
|
||||
session['session_id'],
|
||||
data['timezone'],
|
||||
room
|
||||
])
|
||||
current_app.logger.debug(f'SocketIO: Message offloading for tenant {current_tenant_id}, '
|
||||
f'Question: {task.id}')
|
||||
response = {
|
||||
|
||||
@@ -36,7 +36,7 @@ def ping():
|
||||
|
||||
def detail_question(question, language, model_variables, session_id):
|
||||
current_app.logger.debug(f'Detail question: {question}')
|
||||
current_app.logger.debug(f'model_varialbes: {model_variables}')
|
||||
current_app.logger.debug(f'model_variables: {model_variables}')
|
||||
current_app.logger.debug(f'session_id: {session_id}')
|
||||
retriever = EveAIHistoryRetriever(model_variables=model_variables, session_id=session_id)
|
||||
llm = model_variables['llm']
|
||||
@@ -93,12 +93,6 @@ def ask_question(tenant_id, question, language, session_id, user_timezone, room)
|
||||
current_app.logger.error(f'ask_question: Error initializing chat session in database: {e}')
|
||||
raise
|
||||
|
||||
if tenant.rag_tuning:
|
||||
current_app.rag_tuning_logger.debug(f'Received question for tenant {tenant_id}:\n{question}. Processing...')
|
||||
current_app.rag_tuning_logger.debug(f'Tenant Information: \n{tenant.to_dict()}')
|
||||
current_app.rag_tuning_logger.debug(f'===================================================================')
|
||||
current_app.rag_tuning_logger.debug(f'===================================================================')
|
||||
|
||||
with current_event.create_span("RAG Answer"):
|
||||
result, interaction = answer_using_tenant_rag(question, language, tenant, chat_session)
|
||||
result['algorithm'] = current_app.config['INTERACTION_ALGORITHMS']['RAG_TENANT']['name']
|
||||
@@ -138,8 +132,7 @@ def answer_using_tenant_rag(question, language, tenant, chat_session):
|
||||
|
||||
with current_event.create_span("Detail Question"):
|
||||
detailed_question = detail_question(question, language, model_variables, chat_session.session_id)
|
||||
current_app.logger.debug(f'Original question:\n {question}\n\nDetailed question: {detailed_question}')
|
||||
if tenant.rag_tuning:
|
||||
if model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Detailed Question for tenant {tenant.id}:\n{question}.')
|
||||
current_app.rag_tuning_logger.debug(f'-------------------------------------------------------------------')
|
||||
new_interaction.detailed_question = detailed_question
|
||||
@@ -153,7 +146,7 @@ def answer_using_tenant_rag(question, language, tenant, chat_session):
|
||||
full_template = replace_variable_in_template(language_template, "{tenant_context}", model_variables['rag_context'])
|
||||
rag_prompt = ChatPromptTemplate.from_template(full_template)
|
||||
setup_and_retrieval = RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
|
||||
if tenant.rag_tuning:
|
||||
if model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Full prompt for tenant {tenant.id}:\n{full_template}.')
|
||||
current_app.rag_tuning_logger.debug(f'-------------------------------------------------------------------')
|
||||
|
||||
@@ -181,7 +174,7 @@ def answer_using_tenant_rag(question, language, tenant, chat_session):
|
||||
current_app.logger.debug(f'ask_question: result answer: {result['answer']}')
|
||||
current_app.logger.debug(f'ask_question: result citations: {result["citations"]}')
|
||||
current_app.logger.debug(f'ask_question: insufficient information: {result["insufficient_info"]}')
|
||||
if tenant.rag_tuning:
|
||||
if model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'ask_question: result answer: {result['answer']}')
|
||||
current_app.rag_tuning_logger.debug(f'ask_question: result citations: {result["citations"]}')
|
||||
current_app.rag_tuning_logger.debug(f'ask_question: insufficient information: {result["insufficient_info"]}')
|
||||
@@ -197,7 +190,7 @@ def answer_using_tenant_rag(question, language, tenant, chat_session):
|
||||
)
|
||||
existing_embedding_ids = [emb.id for emb in embeddings]
|
||||
urls = list(set(emb.document_version.url for emb in embeddings))
|
||||
if tenant.rag_tuning:
|
||||
if model_variables['rag_tuning']:
|
||||
current_app.rag_tuning_logger.debug(f'Referenced documents for answer for tenant {tenant.id}:\n')
|
||||
current_app.rag_tuning_logger.debug(f'{urls}')
|
||||
current_app.rag_tuning_logger.debug(f'-------------------------------------------------------------------')
|
||||
|
||||
@@ -5,13 +5,14 @@ from datetime import datetime as dt, timezone as tz, datetime
|
||||
from celery import states
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from flask import current_app
|
||||
from sqlalchemy import or_, and_
|
||||
from sqlalchemy import or_, and_, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from common.extensions import db
|
||||
from common.models.user import Tenant
|
||||
from common.models.entitlements import BusinessEventLog, LicenseUsage, License
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.eveai_exceptions import EveAINoLicenseForTenant
|
||||
from common.utils.eveai_exceptions import EveAINoLicenseForTenant, EveAIException
|
||||
from common.utils.database import Database
|
||||
|
||||
|
||||
# Healthcheck task
|
||||
@@ -24,32 +25,54 @@ def ping():
|
||||
def update_usages():
|
||||
current_timestamp = dt.now(tz.utc)
|
||||
tenant_ids = get_all_tenant_ids()
|
||||
|
||||
# List to collect all errors
|
||||
error_list = []
|
||||
|
||||
for tenant_id in tenant_ids:
|
||||
tenant = Tenant.query.get(tenant_id)
|
||||
if tenant.storage_dirty:
|
||||
recalculate_storage_for_tenant(tenant)
|
||||
check_and_create_license_usage_for_tenant(tenant_id)
|
||||
logs = get_logs_for_processing(tenant_id, current_timestamp)
|
||||
if not logs:
|
||||
continue # If no logs to be processed, continu to the next tenant
|
||||
try:
|
||||
Database(tenant_id).switch_schema()
|
||||
check_and_create_license_usage_for_tenant(tenant_id)
|
||||
tenant = Tenant.query.get(tenant_id)
|
||||
if tenant.storage_dirty:
|
||||
recalculate_storage_for_tenant(tenant)
|
||||
logs = get_logs_for_processing(tenant_id, current_timestamp)
|
||||
if not logs:
|
||||
continue # If no logs to be processed, continu to the next tenant
|
||||
|
||||
# Get the min and max timestamp from the logs
|
||||
min_timestamp = min(log.timestamp for log in logs)
|
||||
max_timestamp = max(log.timestamp for log in logs)
|
||||
# Get the min and max timestamp from the logs
|
||||
min_timestamp = min(log.timestamp for log in logs)
|
||||
max_timestamp = max(log.timestamp for log in logs)
|
||||
|
||||
# Retrieve relevant LicenseUsage records
|
||||
license_usages = get_relevant_license_usages(db.session, tenant_id, min_timestamp, max_timestamp)
|
||||
# Retrieve relevant LicenseUsage records
|
||||
current_app.logger.debug(f"Searching relevant usages for tenant {tenant_id}")
|
||||
license_usages = get_relevant_license_usages(db.session, tenant_id, min_timestamp, max_timestamp)
|
||||
current_app.logger.debug(f"Found {license_usages}, end searching relevant usages for tenant {tenant_id}")
|
||||
|
||||
# Split logs based on LicenseUsage periods
|
||||
logs_by_usage = split_logs_by_license_usage(logs, license_usages)
|
||||
# Split logs based on LicenseUsage periods
|
||||
current_app.logger.debug(f"Splitting usages for tenant {tenant_id}")
|
||||
logs_by_usage = split_logs_by_license_usage(logs, license_usages)
|
||||
current_app.logger.debug(f"Found {logs_by_usage}, end splitting logs for tenant {tenant_id}")
|
||||
|
||||
# Now you can process logs for each LicenseUsage
|
||||
for license_usage_id, logs in logs_by_usage.items():
|
||||
process_logs_for_license_usage(tenant_id, license_usage_id, logs)
|
||||
# Now you can process logs for each LicenseUsage
|
||||
for license_usage_id, logs in logs_by_usage.items():
|
||||
current_app.logger.debug(f"Processing logs for usage id {license_usage_id} for tenant {tenant_id}")
|
||||
process_logs_for_license_usage(tenant_id, license_usage_id, logs)
|
||||
current_app.logger.debug(f"Finished processing logs for tenant {tenant_id}")
|
||||
except Exception as e:
|
||||
error = f"Usage Calculation error for Tenant {tenant_id}: {e}"
|
||||
error_list.append(error)
|
||||
current_app.logger.error(error)
|
||||
continue
|
||||
|
||||
if error_list:
|
||||
raise Exception('\n'.join(error_list))
|
||||
|
||||
return "Update Usages taks completed successfully"
|
||||
|
||||
|
||||
def get_all_tenant_ids():
|
||||
tenant_ids = db.session.query(Tenant.tenant_id).all()
|
||||
tenant_ids = db.session.query(Tenant.id).all()
|
||||
return [tenant_id[0] for tenant_id in tenant_ids] # Extract tenant_id from tuples
|
||||
|
||||
|
||||
@@ -57,21 +80,21 @@ def check_and_create_license_usage_for_tenant(tenant_id):
|
||||
current_date = dt.now(tz.utc).date()
|
||||
license_usages = (db.session.query(LicenseUsage)
|
||||
.filter_by(tenant_id=tenant_id)
|
||||
.filter_by(and_(LicenseUsage.period_start_date <= current_date,
|
||||
LicenseUsage.period_end_date >= current_date))
|
||||
.filter(and_(LicenseUsage.period_start_date <= current_date,
|
||||
LicenseUsage.period_end_date >= current_date))
|
||||
.all())
|
||||
if not license_usages:
|
||||
active_license = (db.session.query(License).filter_by(tenant_id=tenant_id)
|
||||
.filter_by(and_(License.start_date <= current_date,
|
||||
License.end_date >= current_date))
|
||||
.one())
|
||||
.filter(and_(License.start_date <= current_date,
|
||||
License.end_date >= current_date))
|
||||
.one_or_none())
|
||||
if not active_license:
|
||||
current_app.logger.error(f"No License defined for {tenant_id}. "
|
||||
f"Impossible to calculate license usage.")
|
||||
raise EveAINoLicenseForTenant(message=f"No License defined for {tenant_id}. "
|
||||
f"Impossible to calculate license usage.")
|
||||
|
||||
start_date, end_date = calculate_valid_period(current_date, active_license.period_start_date)
|
||||
start_date, end_date = calculate_valid_period(current_date, active_license.start_date)
|
||||
new_license_usage = LicenseUsage(period_start_date=start_date,
|
||||
period_end_date=end_date,
|
||||
license_id=active_license.id,
|
||||
@@ -124,8 +147,8 @@ def get_relevant_license_usages(session, tenant_id, min_timestamp, max_timestamp
|
||||
# Fetch LicenseUsage records where the log timestamps fall between period_start_date and period_end_date
|
||||
return session.query(LicenseUsage).filter(
|
||||
LicenseUsage.tenant_id == tenant_id,
|
||||
LicenseUsage.period_start_date <= max_timestamp,
|
||||
LicenseUsage.period_end_date >= min_timestamp
|
||||
LicenseUsage.period_start_date <= max_timestamp.date(),
|
||||
LicenseUsage.period_end_date >= min_timestamp.date()
|
||||
).order_by(LicenseUsage.period_start_date).all()
|
||||
|
||||
|
||||
@@ -136,7 +159,7 @@ def split_logs_by_license_usage(logs, license_usages):
|
||||
for log in logs:
|
||||
# Find the corresponding LicenseUsage for each log based on the timestamp
|
||||
for license_usage in license_usages:
|
||||
if license_usage.period_start_date <= log.timestamp <= license_usage.period_end_date:
|
||||
if license_usage.period_start_date <= log.timestamp.date() <= license_usage.period_end_date:
|
||||
logs_by_usage[license_usage.id].append(log)
|
||||
break
|
||||
|
||||
@@ -181,7 +204,7 @@ def process_logs_for_license_usage(tenant_id, license_usage_id, logs):
|
||||
log.license_usage_id = license_usage_id
|
||||
|
||||
# Update the LicenseUsage record with the accumulated values
|
||||
license_usage.embedding_mb += embedding_mb_used
|
||||
license_usage.embedding_mb_used += embedding_mb_used
|
||||
license_usage.embedding_prompt_tokens_used += embedding_prompt_tokens_used
|
||||
license_usage.embedding_completion_tokens_used += embedding_completion_tokens_used
|
||||
license_usage.embedding_total_tokens_used += embedding_total_tokens_used
|
||||
@@ -189,27 +212,31 @@ def process_logs_for_license_usage(tenant_id, license_usage_id, logs):
|
||||
license_usage.interaction_completion_tokens_used += interaction_completion_tokens_used
|
||||
license_usage.interaction_total_tokens_used += interaction_total_tokens_used
|
||||
|
||||
current_app.logger.debug(f"Processed logs for license usage {license_usage.id}:\n{license_usage}")
|
||||
|
||||
# Commit the updates to the LicenseUsage and log records
|
||||
try:
|
||||
db.session.add(license_usage)
|
||||
db.session.add(logs)
|
||||
for log in logs:
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"Error trying to update license usage and logs for tenant {tenant_id}. ")
|
||||
current_app.logger.error(f"Error trying to update license usage and logs for tenant {tenant_id}: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
def recalculate_storage_for_tenant(tenant):
|
||||
# Perform a SUM operation to get the total file size from document_versions
|
||||
total_storage = db.session.execute(f"""
|
||||
total_storage = db.session.execute(text(f"""
|
||||
SELECT SUM(file_size)
|
||||
FROM {tenant.id}.document_versions
|
||||
""").scalar()
|
||||
FROM document_version
|
||||
""")).scalar()
|
||||
current_app.logger.debug(f"Recalculating storage for tenant {tenant} - Total storage: {total_storage}")
|
||||
|
||||
# Update the LicenseUsage with the recalculated storage
|
||||
license_usage = db.session.query(LicenseUsage).filter_by(tenant_id=tenant.id).first()
|
||||
license_usage.storage_mb = total_storage / (1024 * 1024) # Convert bytes to MB
|
||||
license_usage.storage_mb_used = total_storage
|
||||
|
||||
# Reset the dirty flag after recalculating
|
||||
tenant.storage_dirty = False
|
||||
|
||||
@@ -27,10 +27,8 @@ class AudioProcessor(TranscriptionProcessor):
|
||||
def _get_transcription(self):
|
||||
file_data = minio_client.download_document_file(
|
||||
self.tenant.id,
|
||||
self.document_version.doc_id,
|
||||
self.document_version.language,
|
||||
self.document_version.id,
|
||||
self.document_version.file_name
|
||||
self.document_version.bucket_name,
|
||||
self.document_version.object_name,
|
||||
)
|
||||
|
||||
with current_event.create_span("Audio Compression"):
|
||||
|
||||
@@ -15,6 +15,7 @@ class HTMLProcessor(Processor):
|
||||
self.html_end_tags = model_variables['html_end_tags']
|
||||
self.html_included_elements = model_variables['html_included_elements']
|
||||
self.html_excluded_elements = model_variables['html_excluded_elements']
|
||||
self.html_excluded_classes = model_variables['html_excluded_classes']
|
||||
self.chunk_size = model_variables['processing_chunk_size'] # Adjust this based on your LLM's optimal input size
|
||||
self.chunk_overlap = model_variables[
|
||||
'processing_chunk_overlap'] # Adjust for context preservation between chunks
|
||||
@@ -24,10 +25,8 @@ class HTMLProcessor(Processor):
|
||||
try:
|
||||
file_data = minio_client.download_document_file(
|
||||
self.tenant.id,
|
||||
self.document_version.doc_id,
|
||||
self.document_version.language,
|
||||
self.document_version.id,
|
||||
self.document_version.file_name
|
||||
self.document_version.bucket_name,
|
||||
self.document_version.object_name,
|
||||
)
|
||||
html_content = file_data.decode('utf-8')
|
||||
|
||||
@@ -47,7 +46,7 @@ class HTMLProcessor(Processor):
|
||||
self._log(f'Parsing HTML for tenant {self.tenant.id}')
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
extracted_html = ''
|
||||
excluded_classes = self._parse_excluded_classes(self.tenant.html_excluded_classes)
|
||||
excluded_classes = self._parse_excluded_classes(self.html_excluded_classes)
|
||||
|
||||
if self.html_included_elements:
|
||||
elements_to_parse = soup.find_all(self.html_included_elements)
|
||||
|
||||
@@ -27,10 +27,8 @@ class PDFProcessor(Processor):
|
||||
try:
|
||||
file_data = minio_client.download_document_file(
|
||||
self.tenant.id,
|
||||
self.document_version.doc_id,
|
||||
self.document_version.language,
|
||||
self.document_version.id,
|
||||
self.document_version.file_name
|
||||
self.document_version.bucket_name,
|
||||
self.document_version.object_name,
|
||||
)
|
||||
|
||||
with current_event.create_span("PDF Extraction"):
|
||||
|
||||
@@ -7,10 +7,8 @@ class SRTProcessor(TranscriptionProcessor):
|
||||
def _get_transcription(self):
|
||||
file_data = minio_client.download_document_file(
|
||||
self.tenant.id,
|
||||
self.document_version.doc_id,
|
||||
self.document_version.language,
|
||||
self.document_version.id,
|
||||
self.document_version.file_name
|
||||
self.document_version.bucket_name,
|
||||
self.document_version.object_name,
|
||||
)
|
||||
srt_content = file_data.decode('utf-8')
|
||||
return self._clean_srt(srt_content)
|
||||
|
||||
@@ -44,3 +44,4 @@ def register_extensions(app):
|
||||
|
||||
|
||||
app, celery = create_app()
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from langchain_core.runnables import RunnablePassthrough
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.extensions import db, minio_client
|
||||
from common.models.document import DocumentVersion, Embedding
|
||||
from common.models.document import DocumentVersion, Embedding, Document
|
||||
from common.models.user import Tenant
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.database import Database
|
||||
@@ -36,34 +36,40 @@ def ping():
|
||||
|
||||
@current_celery.task(name='create_embeddings', queue='embeddings')
|
||||
def create_embeddings(tenant_id, document_version_id):
|
||||
# Retrieve document version to process
|
||||
document_version = DocumentVersion.query.get(document_version_id)
|
||||
if document_version is None:
|
||||
raise Exception(f'Document version {document_version_id} not found')
|
||||
try:
|
||||
# Retrieve Tenant for which we are processing
|
||||
tenant = Tenant.query.get(tenant_id)
|
||||
if tenant is None:
|
||||
raise Exception(f'Tenant {tenant_id} not found')
|
||||
|
||||
# Ensure we are working in the correct database schema
|
||||
Database(tenant_id).switch_schema()
|
||||
|
||||
# Retrieve document version to process
|
||||
document_version = DocumentVersion.query.get(document_version_id)
|
||||
if document_version is None:
|
||||
raise Exception(f'Document version {document_version_id} not found')
|
||||
|
||||
# Retrieve the Catalog ID
|
||||
doc = Document.query.get_or_404(document_version.doc_id)
|
||||
catalog_id = doc.catalog_id
|
||||
|
||||
# Select variables to work with depending on tenant and model
|
||||
model_variables = select_model_variables(tenant, catalog_id=catalog_id)
|
||||
current_app.logger.debug(f'Model variables: {model_variables}')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Create Embeddings request received '
|
||||
f'for non existing document version {document_version_id} '
|
||||
f'for tenant {tenant_id}, '
|
||||
f'error: {e}')
|
||||
raise
|
||||
|
||||
# BusinessEvent creates a context, which is why we need to use it with a with block
|
||||
with BusinessEvent('Create Embeddings', tenant_id,
|
||||
document_version_id=document_version_id,
|
||||
document_version_file_size=document_version.file_size):
|
||||
current_app.logger.info(f'Creating embeddings for tenant {tenant_id} on document version {document_version_id}')
|
||||
try:
|
||||
# Retrieve Tenant for which we are processing
|
||||
tenant = Tenant.query.get(tenant_id)
|
||||
if tenant is None:
|
||||
raise Exception(f'Tenant {tenant_id} not found')
|
||||
|
||||
# Ensure we are working in the correct database schema
|
||||
Database(tenant_id).switch_schema()
|
||||
|
||||
# Select variables to work with depending on tenant and model
|
||||
model_variables = select_model_variables(tenant)
|
||||
current_app.logger.debug(f'Model variables: {model_variables}')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Create Embeddings request received '
|
||||
f'for non existing document version {document_version_id} '
|
||||
f'for tenant {tenant_id}, '
|
||||
f'error: {e}')
|
||||
raise
|
||||
|
||||
try:
|
||||
db.session.add(document_version)
|
||||
@@ -204,7 +210,7 @@ def enrich_chunks(tenant, model_variables, document_version, title, chunks):
|
||||
if len(chunks) > 1:
|
||||
summary = summarize_chunk(tenant, model_variables, document_version, chunks[0])
|
||||
|
||||
chunk_total_context = (f'Filename: {document_version.file_name}\n'
|
||||
chunk_total_context = (f'Filename: {document_version.object_name}\n'
|
||||
f'User Context:\n{document_version.user_context}\n\n'
|
||||
f'User Metadata:\n{document_version.user_metadata}\n\n'
|
||||
f'Title: {title}\n'
|
||||
@@ -213,7 +219,7 @@ def enrich_chunks(tenant, model_variables, document_version, title, chunks):
|
||||
f'System Metadata:\n{document_version.system_metadata}\n\n'
|
||||
)
|
||||
enriched_chunks = []
|
||||
initial_chunk = (f'Filename: {document_version.file_name}\n'
|
||||
initial_chunk = (f'Filename: {document_version.object_name}\n'
|
||||
f'User Context:\n{document_version.user_context}\n\n'
|
||||
f'User Metadata:\n{document_version.user_metadata}\n\n'
|
||||
f'Title: {title}\n'
|
||||
@@ -304,13 +310,12 @@ def log_parsing_info(tenant, tags, included_elements, excluded_elements, exclude
|
||||
def create_potential_chunks_for_markdown(tenant_id, document_version, input_file):
|
||||
try:
|
||||
current_app.logger.info(f'Creating potential chunks for tenant {tenant_id}')
|
||||
markdown_on = document_version.object_name.rsplit('.', 1)[0] + '.md'
|
||||
|
||||
# Download the markdown file from MinIO
|
||||
markdown_data = minio_client.download_document_file(tenant_id,
|
||||
document_version.doc_id,
|
||||
document_version.language,
|
||||
document_version.id,
|
||||
input_file
|
||||
document_version.bucket_name,
|
||||
markdown_on,
|
||||
)
|
||||
markdown = markdown_data.decode('utf-8')
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ No additional configuration is needed; the plugin will automatically detect the
|
||||
|
||||
## Versions
|
||||
|
||||
### 1.1.1 - Add Reinitialisation functionality
|
||||
|
||||
### 1.1.0 - Add Catalog Functionality
|
||||
|
||||
### 1.0.x - Bugfixing Releases
|
||||
|
||||
### 1.0.0 - Initial Release
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: EveAI Sync
|
||||
* Plugin URI: https://askeveai.com/
|
||||
* Description: Synchronizes WordPress content with EveAI API.
|
||||
* Version: 1.0.16
|
||||
* Version: 1.1.1
|
||||
* Author: Josako, Pieter Laroy
|
||||
* Author URI: https://askeveai.com/about/
|
||||
* License: GPL v2 or later
|
||||
@@ -17,7 +17,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define('EVEAI_SYNC_VERSION', '1.0.0');
|
||||
define('EVEAI_SYNC_VERSION', '1.1.1');
|
||||
define('EVEAI_SYNC_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('EVEAI_SYNC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
@@ -50,6 +50,30 @@ function eveai_delete_post_meta($post_id) {
|
||||
}
|
||||
add_action('before_delete_post', 'eveai_delete_post_meta');
|
||||
|
||||
// Clean metadata from Wordpress site
|
||||
function eveai_reinitialize_site() {
|
||||
check_ajax_referer('eveai_reinitialize_site', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('You do not have permission to perform this action.');
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Remove all EveAI-related post meta
|
||||
$wpdb->query("DELETE FROM $wpdb->postmeta WHERE meta_key LIKE '_eveai_%'");
|
||||
|
||||
// Remove all EveAI-related options
|
||||
delete_option('eveai_last_sync_time');
|
||||
delete_option('eveai_sync_status');
|
||||
|
||||
// Optionally, you might want to clear any custom tables if you have any
|
||||
|
||||
wp_send_json_success('Site reinitialized. All EveAI metadata has been removed.');
|
||||
}
|
||||
add_action('wp_ajax_eveai_reinitialize_site', 'eveai_reinitialize_site');
|
||||
|
||||
// Display sync info in post
|
||||
function eveai_display_sync_info($post) {
|
||||
$document_id = get_post_meta($post->ID, '_eveai_document_id', true);
|
||||
|
||||
@@ -16,6 +16,7 @@ class EveAI_Admin {
|
||||
register_setting('eveai_settings', 'eveai_excluded_categories');
|
||||
register_setting('eveai_settings', 'eveai_access_token');
|
||||
register_setting('eveai_settings', 'eveai_token_expiry');
|
||||
register_setting('eveai_settings', 'eveai_catalog_id');
|
||||
}
|
||||
|
||||
public function add_admin_menu() {
|
||||
@@ -50,6 +51,10 @@ class EveAI_Admin {
|
||||
<th scope="row">API Key</th>
|
||||
<td><input type="text" name="eveai_api_key" value="<?php echo esc_attr(get_option('eveai_api_key')); ?>" style="width: 100%;" /></td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<th scope="row">Catalog ID</th>
|
||||
<td><input type="text" name="eveai_catalog_id" value="<?php echo esc_attr(get_option('eveai_catalog_id')); ?>" style="width: 100%;" /></td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<th scope="row">Default Language</th>
|
||||
<td><input type="text" name="eveai_default_language" value="<?php echo esc_attr(get_option('eveai_default_language', 'en')); ?>" style="width: 100%;" /></td>
|
||||
@@ -71,6 +76,11 @@ class EveAI_Admin {
|
||||
<?php wp_nonce_field('eveai_bulk_sync', 'eveai_bulk_sync_nonce'); ?>
|
||||
<input type="submit" name="eveai_bulk_sync" class="button button-primary" value="Start Bulk Sync">
|
||||
</form>
|
||||
|
||||
<h2>Reinitialize Site</h2>
|
||||
<p>Click the button below to remove all EveAI metadata from your site. This will reset the sync status for all posts and pages.</p>
|
||||
<button id="eveai-reinitialize" class="button button-secondary">Reinitialize Site</button>
|
||||
|
||||
<div id="eveai-sync-results" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
<script>
|
||||
@@ -105,6 +115,29 @@ class EveAI_Admin {
|
||||
});
|
||||
}
|
||||
});
|
||||
$('#eveai-reinitialize').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (confirm('Are you sure you want to reinitialize? This will remove all EveAI metadata from your site.')) {
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'eveai_reinitialize_site',
|
||||
nonce: '<?php echo wp_create_nonce('eveai_reinitialize_site'); ?>'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert(response.data);
|
||||
} else {
|
||||
alert('Error: ' + response.data);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('An error occurred. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
|
||||
@@ -13,6 +13,7 @@ class EveAI_API {
|
||||
$this->api_key = get_option('eveai_api_key');
|
||||
$this->access_token = get_option('eveai_access_token');
|
||||
$this->token_expiry = get_option('eveai_token_expiry', 0);
|
||||
$this->catalog_id = get_option('eveai_catalog_id');
|
||||
}
|
||||
|
||||
private function ensure_valid_token() {
|
||||
@@ -111,6 +112,7 @@ class EveAI_API {
|
||||
}
|
||||
|
||||
public function add_url($data) {
|
||||
$data['catalog_id'] = get_option('eveai_catalog_id'); // Include catalog_id
|
||||
return $this->make_request('POST', '/api/v1/documents/add_url', $data);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Set storage_dirty flag for all tenants
|
||||
|
||||
Revision ID: 02debd224316
|
||||
Revises: 8fdd7f2965c1
|
||||
Create Date: 2024-10-08 06:53:17.261709
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '02debd224316'
|
||||
down_revision = '8fdd7f2965c1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute('UPDATE tenant SET storage_dirty = TRUE')
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -0,0 +1,46 @@
|
||||
"""LicenseUsage: correct mb fields to be floats iso integers
|
||||
|
||||
Revision ID: a678c84d5633
|
||||
Revises: 02debd224316
|
||||
Create Date: 2024-10-11 08:03:22.823327
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a678c84d5633'
|
||||
down_revision = '02debd224316'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('license_usage', schema=None) as batch_op:
|
||||
batch_op.alter_column('storage_mb_used',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.Float(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('embedding_mb_used',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.Float(),
|
||||
existing_nullable=True)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('license_usage', schema=None) as batch_op:
|
||||
batch_op.alter_column('embedding_mb_used',
|
||||
existing_type=sa.Float(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=True)
|
||||
batch_op.alter_column('storage_mb_used',
|
||||
existing_type=sa.Float(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=True)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,5 +1,7 @@
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
@@ -19,8 +21,26 @@ config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
# fileConfig(config.config_file_name)
|
||||
# logger = logging.getLogger('alembic.env')
|
||||
|
||||
logging.basicConfig(
|
||||
stream=sys.stdout,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
# Reset handlers to avoid issues with Alembic overriding them
|
||||
for handler in logger.handlers:
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# Add a stream handler to output logs to the console
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||
logger.addHandler(console_handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def get_engine():
|
||||
@@ -124,31 +144,35 @@ def run_migrations_online():
|
||||
with connectable.connect() as connection:
|
||||
tenants = get_tenant_ids()
|
||||
for tenant in tenants:
|
||||
logger.info(f"Migrating tenant: {tenant}")
|
||||
# set search path on the connection, which ensures that
|
||||
# PostgreSQL will emit all CREATE / ALTER / DROP statements
|
||||
# in terms of this schema by default
|
||||
connection.execute(text(f'SET search_path TO "{tenant}", public'))
|
||||
# in SQLAlchemy v2+ the search path change needs to be committed
|
||||
connection.commit()
|
||||
try:
|
||||
os.environ['TENANT_ID'] = str(tenant)
|
||||
logger.info(f"Migrating tenant: {tenant}")
|
||||
# set search path on the connection, which ensures that
|
||||
# PostgreSQL will emit all CREATE / ALTER / DROP statements
|
||||
# in terms of this schema by default
|
||||
connection.execute(text(f'SET search_path TO "{tenant}", public'))
|
||||
# in SQLAlchemy v2+ the search path change needs to be committed
|
||||
connection.commit()
|
||||
|
||||
# make use of non-supported SQLAlchemy attribute to ensure
|
||||
# the dialect reflects tables in terms of the current tenant name
|
||||
connection.dialect.default_schema_name = str(tenant)
|
||||
# make use of non-supported SQLAlchemy attribute to ensure
|
||||
# the dialect reflects tables in terms of the current tenant name
|
||||
connection.dialect.default_schema_name = str(tenant)
|
||||
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
# literal_binds=True,
|
||||
include_object=include_object,
|
||||
)
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
# literal_binds=True,
|
||||
include_object=include_object,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
# for checking migrate or upgrade is running
|
||||
if getattr(config.cmd_opts, "autogenerate", False):
|
||||
break
|
||||
# for checking migrate or upgrade is running
|
||||
if getattr(config.cmd_opts, "autogenerate", False):
|
||||
break
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Adding Catalogs to Document Domain
|
||||
|
||||
Revision ID: 0f5932ff3051
|
||||
Revises: 5a75fb6da7b8
|
||||
Create Date: 2024-10-14 15:20:45.058329
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import pgvector
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0f5932ff3051'
|
||||
down_revision = '5a75fb6da7b8'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('catalog',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('html_tags', postgresql.ARRAY(sa.String(length=10)), nullable=True),
|
||||
sa.Column('html_end_tags', postgresql.ARRAY(sa.String(length=10)), nullable=True),
|
||||
sa.Column('html_included_elements', postgresql.ARRAY(sa.String(length=50)), nullable=True),
|
||||
sa.Column('html_excluded_elements', postgresql.ARRAY(sa.String(length=50)), nullable=True),
|
||||
sa.Column('html_excluded_classes', postgresql.ARRAY(sa.String(length=200)), nullable=True),
|
||||
sa.Column('min_chunk_size', sa.Integer(), nullable=True),
|
||||
sa.Column('max_chunk_size', sa.Integer(), nullable=True),
|
||||
sa.Column('es_k', sa.Integer(), nullable=True),
|
||||
sa.Column('es_similarity_threshold', sa.Float(), nullable=True),
|
||||
sa.Column('chat_RAG_temperature', sa.Float(), nullable=True),
|
||||
sa.Column('chat_no_RAG_temperature', sa.Float(), nullable=True),
|
||||
sa.Column('embed_tuning', sa.Boolean(), nullable=True),
|
||||
sa.Column('rag_tuning', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.add_column('document', sa.Column('catalog_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'document', 'catalog', ['catalog_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('document', 'catalog_id')
|
||||
op.drop_table('catalog')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Upgrading Existing documents to default catalog
|
||||
|
||||
Revision ID: 28984b05d396
|
||||
Revises: 0f5932ff3051
|
||||
Create Date: 2024-10-15 09:02:23.355660
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Session
|
||||
import pgvector
|
||||
from common.models.document import Catalog, Document
|
||||
from common.models.user import Tenant
|
||||
from flask import current_app
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create a console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# Create a logging format
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Add the handler to the logger
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
logger.info("Starting revision upgrade 28984b05d396")
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '28984b05d396'
|
||||
down_revision = '0f5932ff3051'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
logger.info("Starting migration: Creating default catalog and updating documents.")
|
||||
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
try:
|
||||
tenant_id = int(os.getenv('TENANT_ID'))
|
||||
logger.info(f"In migration: tenant_id = {tenant_id}")
|
||||
|
||||
# Step 1: Create a new Default Catalog
|
||||
logger.info("Creating default catalog")
|
||||
default_catalog = Catalog(
|
||||
name='Default Catalog',
|
||||
description=None,
|
||||
)
|
||||
tenant = Tenant.query.get_or_404(tenant_id)
|
||||
default_catalog.html_tags = tenant.html_tags
|
||||
default_catalog.html_end_tags = tenant.html_end_tags
|
||||
default_catalog.html_included_elements = tenant.html_included_elements
|
||||
default_catalog.html_excluded_elements = tenant.html_excluded_elements
|
||||
default_catalog.html_excluded_classes = tenant.html_excluded_classes
|
||||
default_catalog.min_chunk_size = tenant.min_chunk_size
|
||||
default_catalog.max_chunk_size = tenant.max_chunk_size
|
||||
default_catalog.es_k = tenant.es_k
|
||||
default_catalog.es_similarity_threshold = tenant.es_similarity_threshold
|
||||
default_catalog.chat_RAG_temperature = tenant.chat_RAG_temperature
|
||||
default_catalog.chat_no_RAG_temperature = tenant.chat_no_RAG_temperature
|
||||
default_catalog.embed_tuning = tenant.embed_tuning
|
||||
default_catalog.rag_tuning = tenant.rag_tuning
|
||||
|
||||
session.add(default_catalog)
|
||||
session.commit()
|
||||
|
||||
new_catalog_id = default_catalog.id
|
||||
logger.info(f"Default catalog created with ID: {new_catalog_id}")
|
||||
|
||||
# Step 2: Update all documents to use this new catalog
|
||||
logger.info("Updating documents with the new catalog ID.")
|
||||
documents = session.query(Document).all()
|
||||
for document in documents:
|
||||
document.catalog_id = new_catalog_id
|
||||
session.commit()
|
||||
|
||||
logger.info(f"Updated {len(documents)} documents with new catalog ID.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred during migration: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
logger.info("Migration completed successfully.")
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -26,55 +26,55 @@ def upgrade():
|
||||
op.add_column('document_version', sa.Column('object_name', sa.String(length=200), nullable=True))
|
||||
op.add_column('document_version', sa.Column('file_size', sa.Float(), nullable=True))
|
||||
|
||||
# ### Upgrade values for bucket_name, object_name and file_size to reflect minio reality ###
|
||||
from common.models.document import DocumentVersion
|
||||
from common.extensions import minio_client
|
||||
from minio.error import S3Error
|
||||
|
||||
# Create a connection
|
||||
connection = op.get_bind()
|
||||
session = Session(bind=connection)
|
||||
|
||||
# Get the current schema name (which should be the tenant ID)
|
||||
current_schema = connection.execute(text("SELECT current_schema()")).scalar()
|
||||
tenant_id = int(current_schema)
|
||||
|
||||
doc_versions = session.query(DocumentVersion).all()
|
||||
for doc_version in doc_versions:
|
||||
try:
|
||||
object_name = minio_client.generate_object_name(doc_version.doc_id,
|
||||
doc_version.language,
|
||||
doc_version.id,
|
||||
doc_version.file_name)
|
||||
bucket_name = minio_client.generate_bucket_name(tenant_id)
|
||||
doc_version.object_name = object_name
|
||||
doc_version.bucket_name = bucket_name
|
||||
|
||||
try:
|
||||
stat = minio_client.client.stat_object(
|
||||
bucket_name=bucket_name,
|
||||
object_name=object_name
|
||||
)
|
||||
doc_version.file_size = stat.size / 1048576
|
||||
current_app.logger.info(f"Processed Upgrade for DocumentVersion {doc_version.id} for Tenant {tenant_id}")
|
||||
except S3Error as e:
|
||||
if e.code == "NoSuchKey":
|
||||
current_app.logger.warning(
|
||||
f"Object {doc_version.file_location} not found in bucket {doc_version.bucket_name}. Skipping.")
|
||||
continue # Move to the next item
|
||||
else:
|
||||
raise e # Handle other types of S3 errors
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
current_app.logger.error(f"Couldn't process upgrade for DocumentVersion {doc_version.id} for "
|
||||
f"Tenant {tenant_id}. Error: {str(e)}")
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
current_app.logger.info(f"Successfully updated file sizes for tenant schema {current_schema}")
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
current_app.logger.error(f"Error committing changes for tenant schema {current_schema}: {str(e)}")
|
||||
# # ### Upgrade values for bucket_name, object_name and file_size to reflect minio reality ###
|
||||
# from common.models.document import DocumentVersion
|
||||
# from common.extensions import minio_client
|
||||
# from minio.error import S3Error
|
||||
#
|
||||
# # Create a connection
|
||||
# connection = op.get_bind()
|
||||
# session = Session(bind=connection)
|
||||
#
|
||||
# # Get the current schema name (which should be the tenant ID)
|
||||
# current_schema = connection.execute(text("SELECT current_schema()")).scalar()
|
||||
# tenant_id = int(current_schema)
|
||||
#
|
||||
# doc_versions = session.query(DocumentVersion).all()
|
||||
# for doc_version in doc_versions:
|
||||
# try:
|
||||
# object_name = minio_client.generate_object_name(doc_version.doc_id,
|
||||
# doc_version.language,
|
||||
# doc_version.id,
|
||||
# doc_version.file_name)
|
||||
# bucket_name = minio_client.generate_bucket_name(tenant_id)
|
||||
# doc_version.object_name = object_name
|
||||
# doc_version.bucket_name = bucket_name
|
||||
#
|
||||
# try:
|
||||
# stat = minio_client.client.stat_object(
|
||||
# bucket_name=bucket_name,
|
||||
# object_name=object_name
|
||||
# )
|
||||
# doc_version.file_size = stat.size / 1048576
|
||||
# current_app.logger.info(f"Processed Upgrade for DocumentVersion {doc_version.id} for Tenant {tenant_id}")
|
||||
# except S3Error as e:
|
||||
# if e.code == "NoSuchKey":
|
||||
# current_app.logger.warning(
|
||||
# f"Object {doc_version.object_name} not found in bucket {doc_version.bucket_name}. Skipping.")
|
||||
# continue # Move to the next item
|
||||
# else:
|
||||
# raise e # Handle other types of S3 errors
|
||||
# except Exception as e:
|
||||
# session.rollback()
|
||||
# current_app.logger.error(f"Couldn't process upgrade for DocumentVersion {doc_version.id} for "
|
||||
# f"Tenant {tenant_id}. Error: {str(e)}")
|
||||
#
|
||||
# try:
|
||||
# session.commit()
|
||||
# current_app.logger.info(f"Successfully updated file sizes for tenant schema {current_schema}")
|
||||
# except Exception as e:
|
||||
# session.rollback()
|
||||
# current_app.logger.error(f"Error committing changes for tenant schema {current_schema}: {str(e)}")
|
||||
|
||||
# ### commands auto generated by Alembic - Remove old fields ###
|
||||
# op.drop_column('document_version', 'file_location')
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const eveAI = new EveAI(
|
||||
'2',
|
||||
'EveAI-CHAT-9079-8604-7441-6496-7604',
|
||||
'EveAI-CHAT-6525-5692-6412-5828-7546',
|
||||
'http://macstudio.ask-eve-ai-local.com',
|
||||
'en',
|
||||
'en,fr,nl',
|
||||
|
||||
@@ -81,3 +81,4 @@ anthropic~=0.34.2
|
||||
prometheus-client~=0.20.0
|
||||
flower~=2.0.1
|
||||
psutil~=6.0.0
|
||||
celery-redbeat~=2.2.0
|
||||
|
||||
42
scripts/db_squash.sh
Executable file
42
scripts/db_squash.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: ./db_migrate.sh -m "Your migration message" -d migrations/public
|
||||
|
||||
# Load environment variables
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
|
||||
project_dir=$(pwd)
|
||||
# Ensure you're in the correct directory
|
||||
cd "$project_dir" || exit 1
|
||||
|
||||
# Load environment variables (including database connection details)
|
||||
source .env
|
||||
|
||||
# Set Flask app and Python path
|
||||
export FLASK_APP=scripts/run_eveai_app.py
|
||||
export PYTHONPATH="$PYTHONPATH:$project_dir"
|
||||
|
||||
while getopts m: flag
|
||||
do
|
||||
case "${flag}" in
|
||||
m) message=${OPTARG};;
|
||||
*)
|
||||
echo "Invalid option: ${flag}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if the message and directory are provided
|
||||
if [ -z "$message" ]; then
|
||||
echo "Message is required."
|
||||
echo "Usage: ./db_migrate.sh -m \"Your migration message\" -d migrations/public"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run the alembic commands
|
||||
flask db revision --autogenerate -m "Consolidated Public Migrations - $message" -d migrations/public
|
||||
flask db revision --autogenerate -m "Consolidated Tenant Migrations - $message" -d migrations/tenant
|
||||
|
||||
10
scripts/entrypoint_no_db.sh
Executable file
10
scripts/entrypoint_no_db.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Ensure the logs directory has the correct permissions
|
||||
echo "Changing permissions on logs directory"
|
||||
#chown -R appuser:appuser /app/logs
|
||||
chmod -R 777 /app/logs
|
||||
|
||||
# Switch to appuser and execute the command passed to the script
|
||||
exec su -- appuser -c "$@"
|
||||
@@ -1,17 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Delete previous repopack files
|
||||
rm -f *repo.txt
|
||||
|
||||
# Run repopack to generate the file
|
||||
repopack
|
||||
# Define the list of components
|
||||
components=("docker" "eveai_api" "eveai_app" "eveai_beat" "eveai_chat" "eveai_chat_workers" "eveai_entitlements" "eveai_workers" "nginx" "full" "integrations")
|
||||
|
||||
# Check if repopack generated the eveai_repo.txt file
|
||||
if [[ -f "eveai_repo.txt" ]]; then
|
||||
# Get the current timestamp in the format YYYY-DD-MM_HH:MM:SS
|
||||
timestamp=$(date +"%Y-%d-%m_%H-%M-%S")
|
||||
# Get the current date and time in the format YYYY-MM-DD_HH-MM
|
||||
timestamp=$(date +"%Y-%m-%d_%H-%M")
|
||||
|
||||
# Rename the file with the timestamp
|
||||
mv eveai_repo.txt "${timestamp}_eveai_repo.txt"
|
||||
# Loop through each component and perform the tasks
|
||||
for component in "${components[@]}"; do
|
||||
echo "Processing component: $component"
|
||||
|
||||
echo "File renamed to ${timestamp}_eveai_repo.txt"
|
||||
else
|
||||
echo "Error: eveai_repo.txt not found. repopack may have failed."
|
||||
fi
|
||||
# Merge the .repopackignore_base and .repopackignore_<component> into .repopackignore
|
||||
if [[ -f ".repopackignore_base" && -f ".repopackignore_$component" ]]; then
|
||||
cat .repopackignore_base .repopackignore_$component > .repopackignore
|
||||
else
|
||||
echo "Warning: Missing .repopackignore_base or .repopackignore_$component for $component"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Execute repopack
|
||||
repopack
|
||||
|
||||
# Rename the resulting eveai_repo.txt file to <component>YYYY-MM-DD_HH-MM_repo.txt
|
||||
if [[ -f "eveai_repo.txt" ]]; then
|
||||
mv eveai_repo.txt "${component}_${timestamp}_repo.txt"
|
||||
echo "Renamed eveai_repo.txt to ${component}_${timestamp}_repo.txt"
|
||||
else
|
||||
echo "Error: repopack did not generate eveai_repo.txt for $component"
|
||||
fi
|
||||
|
||||
echo "Finished processing $component"
|
||||
done
|
||||
|
||||
@@ -32,10 +32,12 @@ export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR" # In
|
||||
# Run Alembic upgrade for the public schema
|
||||
echo "Applying migrations to the public schema..."
|
||||
flask db upgrade -d "${PROJECT_DIR}/migrations/public"
|
||||
echo "Finished applying migrations to the public schema..."
|
||||
|
||||
# Run Alembic upgrade for the tenant schema
|
||||
echo "Applying migrations to the tenant schema..."
|
||||
flask db upgrade -d "${PROJECT_DIR}/migrations/tenant"
|
||||
echo "Finished applying migrations to the tenant schema..."
|
||||
|
||||
# Set flask environment variables
|
||||
#export FLASK_ENV=development # Use 'production' as appropriate
|
||||
|
||||
17
scripts/start_eveai_beat.sh
Executable file
17
scripts/start_eveai_beat.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "/app/" || exit 1
|
||||
export PROJECT_DIR="/app"
|
||||
export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR" # Include the app directory in the Python path & patched packages
|
||||
|
||||
# Ensure we can write the logs
|
||||
chown -R appuser:appuser /app/logs
|
||||
|
||||
# Start Celery Beat
|
||||
celery -A eveai_beat.celery beat --scheduler=redbeat.RedBeatScheduler --loglevel=debug &
|
||||
|
||||
# Start a worker for the 'llm_interactions' queue with auto-scaling - not necessary, in eveai_chat_workers
|
||||
# celery -A eveai_workers.celery worker --loglevel=info - Q llm_interactions --autoscale=2,8 --hostname=interactions_worker@%h &
|
||||
|
||||
# Wait for all background processes to finish
|
||||
wait
|
||||
17
scripts/start_eveai_entitlements.sh
Executable file
17
scripts/start_eveai_entitlements.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "/app/" || exit 1
|
||||
export PROJECT_DIR="/app"
|
||||
export PYTHONPATH="$PROJECT_DIR/patched_packages:$PYTHONPATH:$PROJECT_DIR" # Include the app directory in the Python path & patched packages
|
||||
|
||||
# Ensure we can write the logs
|
||||
chown -R appuser:appuser /app/logs
|
||||
|
||||
# Start a worker for the 'embeddings' queue with higher concurrency
|
||||
celery -A eveai_entitlements.celery worker --loglevel=debug -Q entitlements --autoscale=2,8 --hostname=entitlements_worker@%h &
|
||||
|
||||
# Start a worker for the 'llm_interactions' queue with auto-scaling - not necessary, in eveai_chat_workers
|
||||
# celery -A eveai_workers.celery worker --loglevel=info - Q llm_interactions --autoscale=2,8 --hostname=interactions_worker@%h &
|
||||
|
||||
# Wait for all background processes to finish
|
||||
wait
|
||||
0
scripts/start_flower.sh
Normal file → Executable file
0
scripts/start_flower.sh
Normal file → Executable file
Reference in New Issue
Block a user