iatoolkit 0.4.2__py3-none-any.whl → 0.66.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iatoolkit/__init__.py +13 -35
- iatoolkit/base_company.py +74 -8
- iatoolkit/cli_commands.py +15 -23
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +46 -0
- iatoolkit/common/routes.py +141 -0
- iatoolkit/common/session_manager.py +24 -0
- iatoolkit/common/util.py +348 -0
- iatoolkit/company_registry.py +7 -8
- iatoolkit/iatoolkit.py +169 -96
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/gemini_adapter.py +356 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_client.py +429 -0
- iatoolkit/infra/llm_proxy.py +139 -0
- iatoolkit/infra/llm_response.py +40 -0
- iatoolkit/infra/mail_app.py +145 -0
- iatoolkit/infra/openai_adapter.py +90 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +144 -0
- iatoolkit/locales/es.yaml +140 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +110 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/llm_query_repo.py +91 -0
- iatoolkit/repositories/models.py +336 -0
- iatoolkit/repositories/profile_repo.py +123 -0
- iatoolkit/repositories/tasks_repo.py +52 -0
- iatoolkit/repositories/vs_repo.py +139 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +6 -6
- iatoolkit/services/branding_service.py +149 -0
- {services → iatoolkit/services}/dispatcher_service.py +39 -99
- {services → iatoolkit/services}/document_service.py +5 -5
- {services → iatoolkit/services}/excel_service.py +27 -21
- {services → iatoolkit/services}/file_processor_service.py +5 -5
- iatoolkit/services/help_content_service.py +30 -0
- {services → iatoolkit/services}/history_service.py +8 -16
- iatoolkit/services/i18n_service.py +104 -0
- {services → iatoolkit/services}/jwt_service.py +18 -27
- iatoolkit/services/language_service.py +77 -0
- {services → iatoolkit/services}/load_documents_service.py +19 -14
- {services → iatoolkit/services}/mail_service.py +5 -5
- iatoolkit/services/onboarding_service.py +43 -0
- {services → iatoolkit/services}/profile_service.py +155 -89
- {services → iatoolkit/services}/prompt_manager_service.py +26 -11
- {services → iatoolkit/services}/query_service.py +142 -104
- {services → iatoolkit/services}/search_service.py +21 -5
- {services → iatoolkit/services}/sql_service.py +24 -6
- {services → iatoolkit/services}/tasks_service.py +10 -10
- iatoolkit/services/user_feedback_service.py +103 -0
- iatoolkit/services/user_session_context_service.py +143 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +112 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +364 -0
- iatoolkit/static/js/chat_onboarding_button.js +97 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +592 -0
- iatoolkit/static/styles/chat_modal.css +169 -0
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/llm_output.css +115 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +5 -15
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/about.html +13 -0
- iatoolkit/templates/base.html +65 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +287 -0
- iatoolkit/templates/chat_modals.html +181 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +50 -0
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +104 -0
- iatoolkit/templates/signup.html +76 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +92 -0
- iatoolkit/views/change_password_view.py +117 -0
- iatoolkit/views/external_login_view.py +73 -0
- iatoolkit/views/file_store_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +72 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +73 -0
- iatoolkit/views/llmquery_api_view.py +57 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +153 -0
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/signup_view.py +94 -0
- iatoolkit/views/tasks_api_view.py +72 -0
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +62 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
- iatoolkit-0.66.2.dist-info/RECORD +119 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.4.2.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
# database_manager.py
|
|
7
|
+
from sqlalchemy import create_engine, event, inspect
|
|
8
|
+
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
9
|
+
from sqlalchemy.engine.url import make_url
|
|
10
|
+
from iatoolkit.repositories.models import Base
|
|
11
|
+
from injector import inject
|
|
12
|
+
from pgvector.psycopg2 import register_vector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DatabaseManager:
|
|
16
|
+
@inject
|
|
17
|
+
def __init__(self, database_url: str, register_pgvector: bool = True):
|
|
18
|
+
"""
|
|
19
|
+
Inicializa el gestor de la base de datos.
|
|
20
|
+
:param database_url: URL de la base de datos.
|
|
21
|
+
:param echo: Si True, habilita logs de SQL.
|
|
22
|
+
"""
|
|
23
|
+
self.url = make_url(database_url)
|
|
24
|
+
if database_url.startswith('sqlite'): # for tests
|
|
25
|
+
self._engine = create_engine(database_url, echo=False)
|
|
26
|
+
else:
|
|
27
|
+
self._engine = create_engine(
|
|
28
|
+
database_url,
|
|
29
|
+
echo=False,
|
|
30
|
+
pool_size=2, # per worker
|
|
31
|
+
max_overflow=3,
|
|
32
|
+
pool_timeout=30,
|
|
33
|
+
pool_recycle=1800,
|
|
34
|
+
pool_pre_ping=True,
|
|
35
|
+
future=True,
|
|
36
|
+
)
|
|
37
|
+
self.SessionFactory = sessionmaker(bind=self._engine,
|
|
38
|
+
autoflush=False,
|
|
39
|
+
autocommit=False,
|
|
40
|
+
expire_on_commit=False)
|
|
41
|
+
self.scoped_session = scoped_session(self.SessionFactory)
|
|
42
|
+
|
|
43
|
+
# REGISTRAR pgvector para cada nueva conexión solo en postgres
|
|
44
|
+
if register_pgvector and self.url.get_backend_name() == 'postgresql':
|
|
45
|
+
event.listen(self._engine, 'connect', self.on_connect)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def on_connect(dbapi_connection, connection_record):
|
|
49
|
+
"""
|
|
50
|
+
Esta función se ejecuta cada vez que se establece una conexión.
|
|
51
|
+
dbapi_connection es la conexión psycopg2 real.
|
|
52
|
+
"""
|
|
53
|
+
register_vector(dbapi_connection)
|
|
54
|
+
|
|
55
|
+
def get_session(self):
|
|
56
|
+
return self.scoped_session()
|
|
57
|
+
|
|
58
|
+
def get_connection(self):
|
|
59
|
+
return self._engine.connect()
|
|
60
|
+
|
|
61
|
+
def get_engine(self):
|
|
62
|
+
return self._engine
|
|
63
|
+
|
|
64
|
+
def create_all(self):
|
|
65
|
+
Base.metadata.create_all(self._engine)
|
|
66
|
+
|
|
67
|
+
def drop_all(self):
|
|
68
|
+
Base.metadata.drop_all(self._engine)
|
|
69
|
+
|
|
70
|
+
def remove_session(self):
|
|
71
|
+
self.scoped_session.remove()
|
|
72
|
+
|
|
73
|
+
def get_table_schema(self,
|
|
74
|
+
table_name: str,
|
|
75
|
+
schema_name: str | None = None,
|
|
76
|
+
exclude_columns: list[str] | None = None) -> str:
|
|
77
|
+
inspector = inspect(self._engine)
|
|
78
|
+
|
|
79
|
+
if table_name not in inspector.get_table_names():
|
|
80
|
+
raise RuntimeError(f"La tabla '{table_name}' no existe en la BD.")
|
|
81
|
+
|
|
82
|
+
if exclude_columns is None:
|
|
83
|
+
exclude_columns = []
|
|
84
|
+
|
|
85
|
+
# get all thre table columns
|
|
86
|
+
columns = inspector.get_columns(table_name)
|
|
87
|
+
|
|
88
|
+
# construct a json dictionary with the table definition
|
|
89
|
+
json_dict = {
|
|
90
|
+
"table": table_name,
|
|
91
|
+
"description": f"Definición de la tabla {table_name}.",
|
|
92
|
+
"fields": []
|
|
93
|
+
}
|
|
94
|
+
if schema_name:
|
|
95
|
+
json_dict["description"] += f"Los detalles de cada campo están en el objeto **`{schema_name}`**."
|
|
96
|
+
|
|
97
|
+
# now add every column to the json dictionary
|
|
98
|
+
for col in columns:
|
|
99
|
+
name = col["name"]
|
|
100
|
+
|
|
101
|
+
# omit the excluded columns.
|
|
102
|
+
if name in exclude_columns:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
json_dict["fields"].append({
|
|
106
|
+
"name": name,
|
|
107
|
+
"type": str(col["type"]),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return "\n\n" + str(json_dict)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.models import Document
|
|
7
|
+
from injector import inject
|
|
8
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
9
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DocumentRepo:
|
|
13
|
+
@inject
|
|
14
|
+
def __init__(self, db_manager: DatabaseManager):
|
|
15
|
+
self.session = db_manager.get_session()
|
|
16
|
+
|
|
17
|
+
def insert(self,new_document: Document):
|
|
18
|
+
self.session.add(new_document)
|
|
19
|
+
self.session.commit()
|
|
20
|
+
return new_document
|
|
21
|
+
|
|
22
|
+
def get(self, company_id, filename: str ) -> Document:
|
|
23
|
+
if not company_id or not filename:
|
|
24
|
+
raise IAToolkitException(IAToolkitException.ErrorType.PARAM_NOT_FILLED,
|
|
25
|
+
'Falta empresa o filename')
|
|
26
|
+
|
|
27
|
+
return self.session.query(Document).filter_by(company_id=company_id, filename=filename).first()
|
|
28
|
+
|
|
29
|
+
def get_by_id(self, document_id: int) -> Document:
|
|
30
|
+
if not document_id:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return self.session.query(Document).filter_by(id=document_id).first()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.models import LLMQuery, Function, Company, Prompt, PromptCategory
|
|
7
|
+
from injector import inject
|
|
8
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
9
|
+
from sqlalchemy import or_
|
|
10
|
+
|
|
11
|
+
class LLMQueryRepo:
|
|
12
|
+
@inject
|
|
13
|
+
def __init__(self, db_manager: DatabaseManager):
|
|
14
|
+
self.session = db_manager.get_session()
|
|
15
|
+
|
|
16
|
+
def add_query(self, query: LLMQuery):
|
|
17
|
+
self.session.add(query)
|
|
18
|
+
self.session.commit()
|
|
19
|
+
return query
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_company_functions(self, company: Company) -> list[Function]:
|
|
23
|
+
return (
|
|
24
|
+
self.session.query(Function)
|
|
25
|
+
.filter(
|
|
26
|
+
Function.is_active.is_(True),
|
|
27
|
+
or_(
|
|
28
|
+
Function.company_id == company.id,
|
|
29
|
+
Function.system_function.is_(True)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
.all()
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def create_or_update_function(self, new_function: Function):
|
|
36
|
+
function = self.session.query(Function).filter_by(company_id=new_function.company_id,
|
|
37
|
+
name=new_function.name).first()
|
|
38
|
+
if function:
|
|
39
|
+
function.description = new_function.description
|
|
40
|
+
function.parameters = new_function.parameters
|
|
41
|
+
function.system_function = new_function.system_function
|
|
42
|
+
else:
|
|
43
|
+
self.session.add(new_function)
|
|
44
|
+
function = new_function
|
|
45
|
+
|
|
46
|
+
self.session.commit()
|
|
47
|
+
return function
|
|
48
|
+
|
|
49
|
+
def create_or_update_prompt(self, new_prompt: Prompt):
|
|
50
|
+
prompt = self.session.query(Prompt).filter_by(company_id=new_prompt.company_id,
|
|
51
|
+
name=new_prompt.name).first()
|
|
52
|
+
if prompt:
|
|
53
|
+
prompt.category_id = new_prompt.category_id
|
|
54
|
+
prompt.description = new_prompt.description
|
|
55
|
+
prompt.order = new_prompt.order
|
|
56
|
+
prompt.active = new_prompt.active
|
|
57
|
+
prompt.is_system_prompt = new_prompt.is_system_prompt
|
|
58
|
+
prompt.filename = new_prompt.filename
|
|
59
|
+
prompt.custom_fields = new_prompt.custom_fields
|
|
60
|
+
else:
|
|
61
|
+
self.session.add(new_prompt)
|
|
62
|
+
prompt = new_prompt
|
|
63
|
+
|
|
64
|
+
self.session.commit()
|
|
65
|
+
return prompt
|
|
66
|
+
|
|
67
|
+
def create_or_update_prompt_category(self, new_category: PromptCategory):
|
|
68
|
+
category = self.session.query(PromptCategory).filter_by(company_id=new_category.company_id,
|
|
69
|
+
name=new_category.name).first()
|
|
70
|
+
if category:
|
|
71
|
+
category.order = new_category.order
|
|
72
|
+
else:
|
|
73
|
+
self.session.add(new_category)
|
|
74
|
+
category = new_category
|
|
75
|
+
|
|
76
|
+
self.session.commit()
|
|
77
|
+
return category
|
|
78
|
+
|
|
79
|
+
def get_history(self, company: Company, user_identifier: str) -> list[LLMQuery]:
|
|
80
|
+
return self.session.query(LLMQuery).filter(
|
|
81
|
+
LLMQuery.user_identifier == user_identifier,
|
|
82
|
+
).filter_by(company_id=company.id).order_by(LLMQuery.created_at.desc()).limit(100).all()
|
|
83
|
+
|
|
84
|
+
def get_prompts(self, company: Company) -> list[Prompt]:
|
|
85
|
+
return self.session.query(Prompt).filter_by(company_id=company.id, is_system_prompt=False).all()
|
|
86
|
+
|
|
87
|
+
def get_system_prompts(self) -> list[Prompt]:
|
|
88
|
+
return self.session.query(Prompt).filter_by(is_system_prompt=True, active=True).order_by(Prompt.order).all()
|
|
89
|
+
|
|
90
|
+
def get_prompt_by_name(self, company: Company, prompt_name: str):
|
|
91
|
+
return self.session.query(Prompt).filter_by(company_id=company.id, name=prompt_name).first()
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
|
|
7
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
8
|
+
from sqlalchemy.orm import relationship, class_mapper, declarative_base
|
|
9
|
+
from sqlalchemy.sql import func
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pgvector.sqlalchemy import Vector
|
|
12
|
+
from enum import Enum as PyEnum
|
|
13
|
+
import secrets
|
|
14
|
+
import enum
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# base class for the ORM
|
|
18
|
+
class Base(DeclarativeBase):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
# relation table for many-to-many relationship between companies and users
|
|
22
|
+
user_company = Table('iat_user_company',
|
|
23
|
+
Base.metadata,
|
|
24
|
+
Column('user_id', Integer,
|
|
25
|
+
ForeignKey('iat_users.id', ondelete='CASCADE'),
|
|
26
|
+
primary_key=True),
|
|
27
|
+
Column('company_id', Integer,
|
|
28
|
+
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
29
|
+
primary_key=True),
|
|
30
|
+
Column('is_active', Boolean, default=True),
|
|
31
|
+
Column('role', String(50), default='user'), # Para manejar roles por empresa
|
|
32
|
+
Column('created_at', DateTime, default=datetime.now)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class ApiKey(Base):
|
|
36
|
+
"""Represents an API key for a company to authenticate against the system."""
|
|
37
|
+
__tablename__ = 'iat_api_keys'
|
|
38
|
+
|
|
39
|
+
id = Column(Integer, primary_key=True)
|
|
40
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
41
|
+
key = Column(String(128), unique=True, nullable=False, index=True) # La API Key en sí
|
|
42
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
43
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
44
|
+
last_used_at = Column(DateTime, nullable=True) # Opcional: para rastrear uso
|
|
45
|
+
|
|
46
|
+
company = relationship("Company", back_populates="api_keys")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Company(Base):
|
|
50
|
+
"""Represents a company or tenant in the multi-tenant system."""
|
|
51
|
+
__tablename__ = 'iat_companies'
|
|
52
|
+
|
|
53
|
+
id = Column(Integer, primary_key=True)
|
|
54
|
+
short_name = Column(String(20), nullable=False, unique=True, index=True)
|
|
55
|
+
name = Column(String(256), nullable=False)
|
|
56
|
+
default_language = Column(String(5), nullable=False, default='es')
|
|
57
|
+
|
|
58
|
+
# encrypted api-key
|
|
59
|
+
openai_api_key = Column(String, nullable=True)
|
|
60
|
+
gemini_api_key = Column(String, nullable=True)
|
|
61
|
+
|
|
62
|
+
branding = Column(JSON, nullable=True)
|
|
63
|
+
onboarding_cards = Column(JSON, nullable=True)
|
|
64
|
+
parameters = Column(JSON, nullable=True)
|
|
65
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
66
|
+
allow_jwt = Column(Boolean, default=True, nullable=True)
|
|
67
|
+
|
|
68
|
+
documents = relationship("Document",
|
|
69
|
+
back_populates="company",
|
|
70
|
+
cascade="all, delete-orphan",
|
|
71
|
+
lazy='dynamic')
|
|
72
|
+
functions = relationship("Function",
|
|
73
|
+
back_populates="company",
|
|
74
|
+
cascade="all, delete-orphan")
|
|
75
|
+
vsdocs = relationship("VSDoc",
|
|
76
|
+
back_populates="company",
|
|
77
|
+
cascade="all, delete-orphan")
|
|
78
|
+
llm_queries = relationship("LLMQuery",
|
|
79
|
+
back_populates="company",
|
|
80
|
+
cascade="all, delete-orphan")
|
|
81
|
+
users = relationship("User",
|
|
82
|
+
secondary=user_company,
|
|
83
|
+
back_populates="companies")
|
|
84
|
+
api_keys = relationship("ApiKey",
|
|
85
|
+
back_populates="company",
|
|
86
|
+
cascade="all, delete-orphan")
|
|
87
|
+
|
|
88
|
+
tasks = relationship("Task", back_populates="company")
|
|
89
|
+
feedbacks = relationship("UserFeedback",
|
|
90
|
+
back_populates="company",
|
|
91
|
+
cascade="all, delete-orphan")
|
|
92
|
+
prompts = relationship("Prompt",
|
|
93
|
+
back_populates="company",
|
|
94
|
+
cascade="all, delete-orphan")
|
|
95
|
+
|
|
96
|
+
def to_dict(self):
|
|
97
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
98
|
+
|
|
99
|
+
# users with rights to use this app
|
|
100
|
+
class User(Base):
|
|
101
|
+
"""Represents an IAToolkit user who can be associated with multiple companies."""
|
|
102
|
+
__tablename__ = 'iat_users'
|
|
103
|
+
|
|
104
|
+
id = Column(Integer, primary_key=True)
|
|
105
|
+
email = Column(String(80), unique=True, nullable=False)
|
|
106
|
+
first_name = Column(String(50), nullable=False)
|
|
107
|
+
last_name = Column(String(50), nullable=False)
|
|
108
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
109
|
+
password = Column(String, nullable=False)
|
|
110
|
+
verified = Column(Boolean, nullable=False, default=False)
|
|
111
|
+
preferred_language = Column(String(5), nullable=True)
|
|
112
|
+
verification_url = Column(String, nullable=True)
|
|
113
|
+
temp_code = Column(String, nullable=True)
|
|
114
|
+
|
|
115
|
+
companies = relationship(
|
|
116
|
+
"Company",
|
|
117
|
+
secondary=user_company,
|
|
118
|
+
back_populates="users",
|
|
119
|
+
cascade="all",
|
|
120
|
+
passive_deletes=True,
|
|
121
|
+
lazy='dynamic'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def to_dict(self):
|
|
125
|
+
return {
|
|
126
|
+
'id': self.id,
|
|
127
|
+
'email': self.email,
|
|
128
|
+
'first_name': self.first_name,
|
|
129
|
+
'last_name': self.last_name,
|
|
130
|
+
'created_at': str(self.created_at),
|
|
131
|
+
'verified': self.verified,
|
|
132
|
+
'companies': [company.to_dict() for company in self.companies]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class Function(Base):
|
|
136
|
+
"""Represents a custom or system function that the LLM can call (tool)."""
|
|
137
|
+
__tablename__ = 'iat_functions'
|
|
138
|
+
|
|
139
|
+
id = Column(Integer, primary_key=True)
|
|
140
|
+
company_id = Column(Integer,
|
|
141
|
+
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
142
|
+
nullable=True)
|
|
143
|
+
name = Column(String(255), nullable=False)
|
|
144
|
+
system_function = Column(Boolean, default=False)
|
|
145
|
+
description = Column(Text, nullable=False)
|
|
146
|
+
parameters = Column(JSON, nullable=False)
|
|
147
|
+
is_active = Column(Boolean, default=True)
|
|
148
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
149
|
+
|
|
150
|
+
company = relationship('Company', back_populates='functions')
|
|
151
|
+
|
|
152
|
+
def to_dict(self):
|
|
153
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Document(Base):
|
|
157
|
+
"""Represents a file or document uploaded by a company for context."""
|
|
158
|
+
__tablename__ = 'iat_documents'
|
|
159
|
+
|
|
160
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
161
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
162
|
+
ondelete='CASCADE'), nullable=False)
|
|
163
|
+
filename = Column(String(256), nullable=False, index=True)
|
|
164
|
+
content = Column(Text, nullable=False)
|
|
165
|
+
content_b64 = Column(Text, nullable=False)
|
|
166
|
+
meta = Column(JSON, nullable=True)
|
|
167
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
168
|
+
|
|
169
|
+
company = relationship("Company", back_populates="documents")
|
|
170
|
+
|
|
171
|
+
def to_dict(self):
|
|
172
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class LLMQuery(Base):
|
|
176
|
+
"""Logs a query made to the LLM, including input, output, and metadata."""
|
|
177
|
+
__tablename__ = 'iat_queries'
|
|
178
|
+
|
|
179
|
+
id = Column(Integer, primary_key=True)
|
|
180
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
181
|
+
ondelete='CASCADE'), nullable=False)
|
|
182
|
+
user_identifier = Column(String(128), nullable=False)
|
|
183
|
+
task_id = Column(Integer, default=0, nullable=True)
|
|
184
|
+
query = Column(Text, nullable=False)
|
|
185
|
+
output = Column(Text, nullable=False)
|
|
186
|
+
response = Column(JSON, nullable=True, default={})
|
|
187
|
+
valid_response = Column(Boolean, nullable=False, default=False)
|
|
188
|
+
function_calls = Column(JSON, nullable=True, default={})
|
|
189
|
+
stats = Column(JSON, default={})
|
|
190
|
+
answer_time = Column(Integer, default=0)
|
|
191
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
192
|
+
|
|
193
|
+
company = relationship("Company", back_populates="llm_queries")
|
|
194
|
+
tasks = relationship("Task", back_populates="llm_query")
|
|
195
|
+
|
|
196
|
+
def to_dict(self):
|
|
197
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class VSDoc(Base):
|
|
201
|
+
"""Stores a text chunk and its corresponding vector embedding for similarity search."""
|
|
202
|
+
__tablename__ = "iat_vsdocs"
|
|
203
|
+
|
|
204
|
+
id = Column(Integer, primary_key=True)
|
|
205
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
206
|
+
ondelete='CASCADE'), nullable=False)
|
|
207
|
+
document_id = Column(Integer, ForeignKey('iat_documents.id',
|
|
208
|
+
ondelete='CASCADE'), nullable=False)
|
|
209
|
+
text = Column(Text, nullable=False)
|
|
210
|
+
embedding = Column(Vector(384), nullable=False) # Ajusta la dimensión si es necesario
|
|
211
|
+
|
|
212
|
+
company = relationship("Company", back_populates="vsdocs")
|
|
213
|
+
|
|
214
|
+
def to_dict(self):
|
|
215
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
216
|
+
|
|
217
|
+
class TaskStatus(PyEnum):
|
|
218
|
+
"""Enumeration for the possible statuses of a Task."""
|
|
219
|
+
pendiente = "pendiente" # task created and waiting to be executed.
|
|
220
|
+
ejecutado = "ejecutado" # the IA algorithm has been executed.
|
|
221
|
+
aprobada = "aprobada" # validated and approved by human.
|
|
222
|
+
rechazada = "rechazada" # validated and rejected by human.
|
|
223
|
+
fallida = "fallida" # error executing the IA algorithm.
|
|
224
|
+
|
|
225
|
+
class TaskType(Base):
|
|
226
|
+
"""Defines a type of task that can be executed, including its prompt template."""
|
|
227
|
+
__tablename__ = 'iat_task_types'
|
|
228
|
+
|
|
229
|
+
id = Column(Integer, primary_key=True)
|
|
230
|
+
name = Column(String(100), unique=True, nullable=False)
|
|
231
|
+
prompt_template = Column(String(100), nullable=True) # Plantilla de prompt por defecto.
|
|
232
|
+
template_args = Column(JSON, nullable=True) # Argumentos/prefijos de configuración para el template.
|
|
233
|
+
|
|
234
|
+
class Task(Base):
|
|
235
|
+
"""Represents an asynchronous task to be executed by the system, often involving an LLM."""
|
|
236
|
+
__tablename__ = 'iat_tasks'
|
|
237
|
+
|
|
238
|
+
id = Column(Integer, primary_key=True)
|
|
239
|
+
company_id = Column(Integer, ForeignKey("iat_companies.id"))
|
|
240
|
+
|
|
241
|
+
user_id = Column(Integer, nullable=True, default=0)
|
|
242
|
+
task_type_id = Column(Integer, ForeignKey('iat_task_types.id'), nullable=False)
|
|
243
|
+
status = Column(Enum(TaskStatus, name="task_status_enum"),
|
|
244
|
+
default=TaskStatus.pendiente, nullable=False)
|
|
245
|
+
client_data = Column(JSON, nullable=True, default={})
|
|
246
|
+
company_task_id = Column(Integer, nullable=True, default=0)
|
|
247
|
+
execute_at = Column(DateTime, default=datetime.now, nullable=True)
|
|
248
|
+
llm_query_id = Column(Integer, ForeignKey('iat_queries.id'), nullable=True)
|
|
249
|
+
callback_url = Column(String(512), default=None, nullable=True)
|
|
250
|
+
files = Column(JSON, default=[], nullable=True)
|
|
251
|
+
|
|
252
|
+
review_user = Column(String(128), nullable=True, default='')
|
|
253
|
+
review_date = Column(DateTime, nullable=True)
|
|
254
|
+
comment = Column(Text, nullable=True)
|
|
255
|
+
approved = Column(Boolean, nullable=False, default=False)
|
|
256
|
+
|
|
257
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
258
|
+
updated_at = Column(DateTime, default=datetime.now)
|
|
259
|
+
|
|
260
|
+
task_type = relationship("TaskType")
|
|
261
|
+
llm_query = relationship("LLMQuery", back_populates="tasks", uselist=False)
|
|
262
|
+
company = relationship("Company", back_populates="tasks")
|
|
263
|
+
|
|
264
|
+
class UserFeedback(Base):
|
|
265
|
+
"""Stores feedback and ratings submitted by users for specific interactions."""
|
|
266
|
+
__tablename__ = 'iat_feedback'
|
|
267
|
+
|
|
268
|
+
id = Column(Integer, primary_key=True)
|
|
269
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
270
|
+
ondelete='CASCADE'), nullable=False)
|
|
271
|
+
user_identifier = Column(String(128), default='', nullable=True)
|
|
272
|
+
message = Column(Text, nullable=False)
|
|
273
|
+
rating = Column(Integer, nullable=False)
|
|
274
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
275
|
+
|
|
276
|
+
company = relationship("Company", back_populates="feedbacks")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class PromptCategory(Base):
|
|
280
|
+
"""Represents a category to group and organize prompts."""
|
|
281
|
+
__tablename__ = 'iat_prompt_categories'
|
|
282
|
+
id = Column(Integer, primary_key=True)
|
|
283
|
+
name = Column(String, nullable=False)
|
|
284
|
+
order = Column(Integer, nullable=False, default=0)
|
|
285
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id'), nullable=False)
|
|
286
|
+
|
|
287
|
+
prompts = relationship("Prompt", back_populates="category", order_by="Prompt.order")
|
|
288
|
+
|
|
289
|
+
def __repr__(self):
|
|
290
|
+
return f"<PromptCategory(name='{self.name}', order={self.order})>"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class Prompt(Base):
|
|
294
|
+
"""Represents a system or user-defined prompt template for the LLM."""
|
|
295
|
+
__tablename__ = 'iat_prompt'
|
|
296
|
+
|
|
297
|
+
id = Column(Integer, primary_key=True)
|
|
298
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
299
|
+
ondelete='CASCADE'), nullable=True)
|
|
300
|
+
name = Column(String(64), nullable=False)
|
|
301
|
+
description = Column(String(256), nullable=False)
|
|
302
|
+
filename = Column(String(256), nullable=False)
|
|
303
|
+
active = Column(Boolean, default=True)
|
|
304
|
+
is_system_prompt = Column(Boolean, default=False)
|
|
305
|
+
order = Column(Integer, nullable=False, default=0) # Nuevo campo para el orden
|
|
306
|
+
category_id = Column(Integer, ForeignKey('iat_prompt_categories.id'), nullable=True)
|
|
307
|
+
custom_fields = Column(JSON, nullable=False, default=[])
|
|
308
|
+
|
|
309
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
310
|
+
|
|
311
|
+
company = relationship("Company", back_populates="prompts")
|
|
312
|
+
category = relationship("PromptCategory", back_populates="prompts")
|
|
313
|
+
|
|
314
|
+
class AccessLog(Base):
|
|
315
|
+
# Modelo ORM para registrar cada intento de acceso a la plataforma.
|
|
316
|
+
__tablename__ = 'iat_access_log'
|
|
317
|
+
|
|
318
|
+
id = Column(BigInteger, primary_key=True)
|
|
319
|
+
|
|
320
|
+
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
321
|
+
company_short_name = Column(String(100), nullable=False, index=True)
|
|
322
|
+
user_identifier = Column(String(255), index=True)
|
|
323
|
+
|
|
324
|
+
# Cómo y el Resultado
|
|
325
|
+
auth_type = Column(String(20), nullable=False) # 'local', 'external_api', 'redeem_token', etc.
|
|
326
|
+
outcome = Column(String(10), nullable=False) # 'success' o 'failure'
|
|
327
|
+
reason_code = Column(String(50)) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
|
|
328
|
+
|
|
329
|
+
# Contexto de la Petición
|
|
330
|
+
source_ip = Column(String(45), nullable=False)
|
|
331
|
+
user_agent_hash = Column(String(16)) # Hash corto del User-Agent
|
|
332
|
+
request_path = Column(String(255), nullable=False)
|
|
333
|
+
|
|
334
|
+
def __repr__(self):
|
|
335
|
+
return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
|
|
336
|
+
f"user='{self.user_identifier}', outcome='{self.outcome}')>")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.models import User, Company, ApiKey, UserFeedback
|
|
7
|
+
from injector import inject
|
|
8
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
9
|
+
from sqlalchemy.orm import joinedload # Para cargar la relación eficientemente
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProfileRepo:
|
|
13
|
+
@inject
|
|
14
|
+
def __init__(self, db_manager: DatabaseManager):
|
|
15
|
+
self.session = db_manager.get_session()
|
|
16
|
+
|
|
17
|
+
def get_user_by_id(self, user_id: int) -> User:
|
|
18
|
+
user = self.session.query(User).filter_by(id=user_id).first()
|
|
19
|
+
return user
|
|
20
|
+
|
|
21
|
+
def get_user_by_email(self, email: str) -> User:
|
|
22
|
+
user = self.session.query(User).filter_by(email=email).first()
|
|
23
|
+
return user
|
|
24
|
+
|
|
25
|
+
def create_user(self, new_user: User):
|
|
26
|
+
self.session.add(new_user)
|
|
27
|
+
self.session.commit()
|
|
28
|
+
return new_user
|
|
29
|
+
|
|
30
|
+
def save_user(self,existing_user: User):
|
|
31
|
+
self.session.add(existing_user)
|
|
32
|
+
self.session.commit()
|
|
33
|
+
return existing_user
|
|
34
|
+
|
|
35
|
+
def update_user(self, email, **kwargs):
|
|
36
|
+
user = self.session.query(User).filter_by(email=email).first()
|
|
37
|
+
if not user:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
# get the fields for update
|
|
41
|
+
for key, value in kwargs.items():
|
|
42
|
+
if hasattr(user, key): # Asegura que el campo existe en User
|
|
43
|
+
setattr(user, key, value)
|
|
44
|
+
|
|
45
|
+
self.session.commit()
|
|
46
|
+
return user # return updated object
|
|
47
|
+
|
|
48
|
+
def verify_user(self, email):
|
|
49
|
+
return self.update_user(email, verified=True)
|
|
50
|
+
|
|
51
|
+
def set_temp_code(self, email, temp_code):
|
|
52
|
+
return self.update_user(email, temp_code=temp_code)
|
|
53
|
+
|
|
54
|
+
def reset_temp_code(self, email):
|
|
55
|
+
return self.update_user(email, temp_code=None)
|
|
56
|
+
|
|
57
|
+
def update_password(self, email, hashed_password):
|
|
58
|
+
return self.update_user(email, password=hashed_password)
|
|
59
|
+
|
|
60
|
+
def get_company(self, name: str) -> Company:
|
|
61
|
+
return self.session.query(Company).filter_by(name=name).first()
|
|
62
|
+
|
|
63
|
+
def get_company_by_id(self, company_id: int) -> Company:
|
|
64
|
+
return self.session.query(Company).filter_by(id=company_id).first()
|
|
65
|
+
|
|
66
|
+
def get_company_by_short_name(self, short_name: str) -> Company:
|
|
67
|
+
return self.session.query(Company).filter(Company.short_name == short_name).first()
|
|
68
|
+
|
|
69
|
+
def get_companies(self) -> list[Company]:
|
|
70
|
+
return self.session.query(Company).all()
|
|
71
|
+
|
|
72
|
+
def create_company(self, new_company: Company):
|
|
73
|
+
company = self.session.query(Company).filter_by(name=new_company.name).first()
|
|
74
|
+
if company:
|
|
75
|
+
if company.parameters != new_company.parameters:
|
|
76
|
+
company.parameters = new_company.parameters
|
|
77
|
+
if company.branding != new_company.branding:
|
|
78
|
+
company.branding = new_company.branding
|
|
79
|
+
if company.onboarding_cards != new_company.onboarding_cards:
|
|
80
|
+
company.onboarding_cards = new_company.onboarding_cards
|
|
81
|
+
else:
|
|
82
|
+
# Si la compañía no existe, la añade a la sesión.
|
|
83
|
+
self.session.add(new_company)
|
|
84
|
+
company = new_company
|
|
85
|
+
|
|
86
|
+
self.session.commit()
|
|
87
|
+
return company
|
|
88
|
+
|
|
89
|
+
def save_feedback(self, feedback: UserFeedback):
|
|
90
|
+
self.session.add(feedback)
|
|
91
|
+
self.session.commit()
|
|
92
|
+
return feedback
|
|
93
|
+
|
|
94
|
+
def create_api_key(self, new_api_key: ApiKey):
|
|
95
|
+
self.session.add(new_api_key)
|
|
96
|
+
self.session.commit()
|
|
97
|
+
return new_api_key
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
|
|
101
|
+
"""
|
|
102
|
+
search for an active API Key by its value.
|
|
103
|
+
returns the entry if found and is active, None otherwise.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
# Usamos joinedload para cargar la compañía en la misma consulta
|
|
107
|
+
api_key_entry = self.session.query(ApiKey)\
|
|
108
|
+
.options(joinedload(ApiKey.company))\
|
|
109
|
+
.filter(ApiKey.key == api_key_value, ApiKey.is_active == True)\
|
|
110
|
+
.first()
|
|
111
|
+
return api_key_entry
|
|
112
|
+
except Exception:
|
|
113
|
+
self.session.rollback() # Asegura que la sesión esté limpia tras un error
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def get_active_api_key_by_company(self, company: Company) -> ApiKey | None:
|
|
117
|
+
return self.session.query(ApiKey)\
|
|
118
|
+
.filter(ApiKey.company == company, ApiKey.is_active == True)\
|
|
119
|
+
.first()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|