iatoolkit 0.71.4__py3-none-any.whl → 1.4.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 +19 -7
- iatoolkit/base_company.py +1 -71
- iatoolkit/cli_commands.py +9 -21
- iatoolkit/common/exceptions.py +2 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +38 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +53 -32
- iatoolkit/common/util.py +17 -12
- iatoolkit/company_registry.py +55 -14
- iatoolkit/{iatoolkit.py → core.py} +102 -72
- iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
- iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
- iatoolkit/infra/llm_proxy.py +235 -134
- iatoolkit/infra/llm_response.py +5 -0
- iatoolkit/locales/en.yaml +134 -4
- iatoolkit/locales/es.yaml +293 -162
- iatoolkit/repositories/database_manager.py +92 -22
- iatoolkit/repositories/document_repo.py +7 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +36 -22
- iatoolkit/repositories/models.py +86 -95
- iatoolkit/repositories/profile_repo.py +64 -13
- iatoolkit/repositories/vs_repo.py +31 -28
- iatoolkit/services/auth_service.py +1 -1
- iatoolkit/services/branding_service.py +1 -1
- iatoolkit/services/company_context_service.py +96 -39
- iatoolkit/services/configuration_service.py +329 -67
- iatoolkit/services/dispatcher_service.py +51 -227
- iatoolkit/services/document_service.py +10 -1
- iatoolkit/services/embedding_service.py +9 -6
- iatoolkit/services/excel_service.py +50 -2
- iatoolkit/services/file_processor_service.py +0 -5
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/jwt_service.py +1 -1
- iatoolkit/services/knowledge_base_service.py +412 -0
- iatoolkit/services/language_service.py +8 -2
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +42 -29
- iatoolkit/services/load_documents_service.py +18 -47
- iatoolkit/services/mail_service.py +171 -25
- iatoolkit/services/profile_service.py +69 -36
- iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +136 -25
- iatoolkit/services/query_service.py +229 -203
- iatoolkit/services/sql_service.py +116 -34
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +18 -6
- iatoolkit/services/user_session_context_service.py +121 -51
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +1 -1
- iatoolkit/static/js/chat_help_content.js +4 -4
- iatoolkit/static/js/chat_main.js +61 -9
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/js/chat_reload_button.js +4 -1
- iatoolkit/static/styles/chat_iatoolkit.css +59 -3
- iatoolkit/static/styles/chat_public.css +28 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +223 -7
- iatoolkit/static/styles/llm_output.css +34 -1
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +28 -3
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +30 -5
- iatoolkit/templates/_login_widget.html +3 -3
- iatoolkit/templates/base.html +13 -0
- iatoolkit/templates/chat.html +45 -3
- iatoolkit/templates/forgot_password.html +3 -2
- iatoolkit/templates/onboarding_shell.html +1 -2
- iatoolkit/templates/signup.html +3 -0
- iatoolkit/views/base_login_view.py +8 -3
- iatoolkit/views/change_password_view.py +1 -1
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/forgot_password_view.py +9 -4
- iatoolkit/views/history_api_view.py +3 -3
- iatoolkit/views/home_view.py +4 -2
- iatoolkit/views/init_context_api_view.py +1 -1
- iatoolkit/views/llmquery_api_view.py +4 -3
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +15 -11
- iatoolkit/views/login_view.py +25 -8
- iatoolkit/views/logout_api_view.py +10 -2
- iatoolkit/views/prompt_api_view.py +1 -1
- iatoolkit/views/rag_api_view.py +216 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +12 -4
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +1 -1
- iatoolkit-1.4.2.dist-info/METADATA +268 -0
- iatoolkit-1.4.2.dist-info/RECORD +133 -0
- iatoolkit-1.4.2.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- iatoolkit/repositories/tasks_repo.py +0 -52
- iatoolkit/services/history_service.py +0 -37
- iatoolkit/services/search_service.py +0 -55
- iatoolkit/services/tasks_service.py +0 -188
- iatoolkit/templates/about.html +0 -13
- iatoolkit/templates/index.html +0 -145
- iatoolkit/templates/login_simulation.html +0 -45
- iatoolkit/views/external_login_view.py +0 -73
- iatoolkit/views/index_view.py +0 -14
- iatoolkit/views/login_simulation_view.py +0 -93
- iatoolkit/views/tasks_api_view.py +0 -72
- iatoolkit/views/tasks_review_api_view.py +0 -55
- iatoolkit-0.71.4.dist-info/METADATA +0 -276
- iatoolkit-0.71.4.dist-info/RECORD +0 -122
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,13 @@ class DocumentRepo:
|
|
|
26
26
|
|
|
27
27
|
return self.session.query(Document).filter_by(company_id=company_id, filename=filename).first()
|
|
28
28
|
|
|
29
|
+
def get_by_hash(self, company_id: int, file_hash: str) -> Document:
|
|
30
|
+
"""Find a document by its content hash within a company."""
|
|
31
|
+
if not company_id or not file_hash:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
return self.session.query(Document).filter_by(company_id=company_id, hash=file_hash).first()
|
|
35
|
+
|
|
29
36
|
def get_by_id(self, document_id: int) -> Document:
|
|
30
37
|
if not document_id:
|
|
31
38
|
return None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from iatoolkit.common.interfaces.asset_storage import AssetRepository, AssetType
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FileSystemAssetRepository(AssetRepository):
|
|
6
|
+
def _get_path(self, company_short_name: str, asset_type: AssetType, filename: str = "") -> Path:
|
|
7
|
+
return Path("companies") / company_short_name / asset_type.value / filename
|
|
8
|
+
|
|
9
|
+
def exists(self, company_short_name: str, asset_type: AssetType, filename: str) -> bool:
|
|
10
|
+
return self._get_path(company_short_name, asset_type, filename).is_file()
|
|
11
|
+
|
|
12
|
+
def read_text(self, company_short_name: str, asset_type: AssetType, filename: str) -> str:
|
|
13
|
+
path = self._get_path(company_short_name, asset_type, filename)
|
|
14
|
+
if not path.is_file():
|
|
15
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
16
|
+
return path.read_text(encoding="utf-8")
|
|
17
|
+
|
|
18
|
+
def list_files(self, company_short_name: str, asset_type: AssetType, extension: str = None) -> list[str]:
|
|
19
|
+
directory = self._get_path(company_short_name, asset_type)
|
|
20
|
+
if not directory.exists():
|
|
21
|
+
return []
|
|
22
|
+
files = [f.name for f in directory.iterdir() if f.is_file()]
|
|
23
|
+
if extension:
|
|
24
|
+
files = [f for f in files if f.endswith(extension)]
|
|
25
|
+
return files
|
|
26
|
+
|
|
27
|
+
def write_text(self, company_short_name: str, asset_type: AssetType, filename: str, content: str) -> None:
|
|
28
|
+
path = self._get_path(company_short_name, asset_type, filename)
|
|
29
|
+
# Ensure the directory exists (e.g. creating a new company structure)
|
|
30
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
path.write_text(content, encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
def delete(self, company_short_name: str, asset_type: AssetType, filename: str) -> None:
|
|
34
|
+
path = self._get_path(company_short_name, asset_type, filename)
|
|
35
|
+
if path.exists():
|
|
36
|
+
path.unlink()
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from iatoolkit.repositories.models import LLMQuery,
|
|
6
|
+
from iatoolkit.repositories.models import LLMQuery, Tool, Company, Prompt, PromptCategory
|
|
7
7
|
from injector import inject
|
|
8
8
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
9
9
|
from sqlalchemy import or_
|
|
@@ -13,38 +13,52 @@ class LLMQueryRepo:
|
|
|
13
13
|
def __init__(self, db_manager: DatabaseManager):
|
|
14
14
|
self.session = db_manager.get_session()
|
|
15
15
|
|
|
16
|
+
def commit(self):
|
|
17
|
+
self.session.commit()
|
|
18
|
+
|
|
19
|
+
def rollback(self):
|
|
20
|
+
self.session.rollback()
|
|
21
|
+
|
|
16
22
|
def add_query(self, query: LLMQuery):
|
|
17
23
|
self.session.add(query)
|
|
18
24
|
self.session.commit()
|
|
19
25
|
return query
|
|
20
26
|
|
|
21
27
|
|
|
22
|
-
def
|
|
28
|
+
def get_company_tools(self, company: Company) -> list[Tool]:
|
|
23
29
|
return (
|
|
24
|
-
self.session.query(
|
|
30
|
+
self.session.query(Tool)
|
|
25
31
|
.filter(
|
|
26
|
-
|
|
32
|
+
Tool.is_active.is_(True),
|
|
27
33
|
or_(
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
Tool.company_id == company.id,
|
|
35
|
+
Tool.system_function.is_(True)
|
|
30
36
|
)
|
|
31
37
|
)
|
|
38
|
+
# Ordenamos descendente: True (System) va primero, False (Company) va después
|
|
39
|
+
.order_by(Tool.system_function.desc())
|
|
32
40
|
.all()
|
|
33
41
|
)
|
|
34
42
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
def delete_system_tools(self):
|
|
44
|
+
self.session.query(Tool).filter_by(system_function=True).delete(synchronize_session=False)
|
|
45
|
+
|
|
46
|
+
def create_or_update_tool(self, new_tool: Tool):
|
|
47
|
+
tool = self.session.query(Tool).filter_by(company_id=new_tool.company_id,
|
|
48
|
+
name=new_tool.name).first()
|
|
49
|
+
if tool:
|
|
50
|
+
tool.description = new_tool.description
|
|
51
|
+
tool.parameters = new_tool.parameters
|
|
52
|
+
tool.system_function = new_tool.system_function
|
|
42
53
|
else:
|
|
43
|
-
self.session.add(
|
|
44
|
-
|
|
54
|
+
self.session.add(new_tool)
|
|
55
|
+
tool = new_tool
|
|
45
56
|
|
|
46
|
-
self.session.
|
|
47
|
-
return
|
|
57
|
+
self.session.flush()
|
|
58
|
+
return tool
|
|
59
|
+
|
|
60
|
+
def delete_tool(self, tool: Tool):
|
|
61
|
+
self.session.query(Tool).filter_by(id=tool.id).delete(synchronize_session=False)
|
|
48
62
|
|
|
49
63
|
def create_or_update_prompt(self, new_prompt: Prompt):
|
|
50
64
|
prompt = self.session.query(Prompt).filter_by(company_id=new_prompt.company_id,
|
|
@@ -53,7 +67,6 @@ class LLMQueryRepo:
|
|
|
53
67
|
prompt.category_id = new_prompt.category_id
|
|
54
68
|
prompt.description = new_prompt.description
|
|
55
69
|
prompt.order = new_prompt.order
|
|
56
|
-
prompt.active = new_prompt.active
|
|
57
70
|
prompt.is_system_prompt = new_prompt.is_system_prompt
|
|
58
71
|
prompt.filename = new_prompt.filename
|
|
59
72
|
prompt.custom_fields = new_prompt.custom_fields
|
|
@@ -61,7 +74,7 @@ class LLMQueryRepo:
|
|
|
61
74
|
self.session.add(new_prompt)
|
|
62
75
|
prompt = new_prompt
|
|
63
76
|
|
|
64
|
-
self.session.
|
|
77
|
+
self.session.flush()
|
|
65
78
|
return prompt
|
|
66
79
|
|
|
67
80
|
def create_or_update_prompt_category(self, new_category: PromptCategory):
|
|
@@ -73,7 +86,7 @@ class LLMQueryRepo:
|
|
|
73
86
|
self.session.add(new_category)
|
|
74
87
|
category = new_category
|
|
75
88
|
|
|
76
|
-
self.session.
|
|
89
|
+
self.session.flush()
|
|
77
90
|
return category
|
|
78
91
|
|
|
79
92
|
def get_history(self, company: Company, user_identifier: str) -> list[LLMQuery]:
|
|
@@ -84,8 +97,9 @@ class LLMQueryRepo:
|
|
|
84
97
|
def get_prompts(self, company: Company) -> list[Prompt]:
|
|
85
98
|
return self.session.query(Prompt).filter_by(company_id=company.id, is_system_prompt=False).all()
|
|
86
99
|
|
|
100
|
+
def get_prompt_by_name(self, company: Company, prompt_name: str):
|
|
101
|
+
return self.session.query(Prompt).filter_by(company_id=company.id, name=prompt_name).first()
|
|
102
|
+
|
|
87
103
|
def get_system_prompts(self) -> list[Prompt]:
|
|
88
104
|
return self.session.query(Prompt).filter_by(is_system_prompt=True, active=True).order_by(Prompt.order).all()
|
|
89
105
|
|
|
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()
|
iatoolkit/repositories/models.py
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
|
|
7
7
|
from sqlalchemy.orm import DeclarativeBase
|
|
8
|
-
from sqlalchemy.orm import relationship, class_mapper
|
|
8
|
+
from sqlalchemy.orm import relationship, class_mapper
|
|
9
9
|
from sqlalchemy.sql import func
|
|
10
|
+
from sqlalchemy import UniqueConstraint
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from pgvector.sqlalchemy import Vector
|
|
12
|
-
from enum import Enum as PyEnum
|
|
13
|
-
import secrets
|
|
14
13
|
import enum
|
|
15
14
|
|
|
16
15
|
|
|
@@ -27,8 +26,7 @@ user_company = Table('iat_user_company',
|
|
|
27
26
|
Column('company_id', Integer,
|
|
28
27
|
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
29
28
|
primary_key=True),
|
|
30
|
-
Column('
|
|
31
|
-
Column('role', String(50), default='user'), # Para manejar roles por empresa
|
|
29
|
+
Column('role', String, nullable=True, default='user'),
|
|
32
30
|
Column('created_at', DateTime, default=datetime.now)
|
|
33
31
|
)
|
|
34
32
|
|
|
@@ -36,9 +34,10 @@ class ApiKey(Base):
|
|
|
36
34
|
"""Represents an API key for a company to authenticate against the system."""
|
|
37
35
|
__tablename__ = 'iat_api_keys'
|
|
38
36
|
|
|
39
|
-
id = Column(Integer, primary_key=True)
|
|
37
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
40
38
|
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
41
|
-
|
|
39
|
+
key_name = Column(String, nullable=False)
|
|
40
|
+
key = Column(String, unique=True, nullable=False, index=True) # La API Key en sí
|
|
42
41
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
43
42
|
created_at = Column(DateTime, default=datetime.now)
|
|
44
43
|
last_used_at = Column(DateTime, nullable=True) # Opcional: para rastrear uso
|
|
@@ -50,13 +49,10 @@ class Company(Base):
|
|
|
50
49
|
"""Represents a company or tenant in the multi-tenant system."""
|
|
51
50
|
__tablename__ = 'iat_companies'
|
|
52
51
|
|
|
53
|
-
id = Column(Integer, primary_key=True)
|
|
54
|
-
short_name = Column(String
|
|
55
|
-
name = Column(String
|
|
52
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
53
|
+
short_name = Column(String, nullable=False, unique=True, index=True)
|
|
54
|
+
name = Column(String, nullable=False)
|
|
56
55
|
|
|
57
|
-
# encrypted api-key
|
|
58
|
-
openai_api_key = Column(String, nullable=True)
|
|
59
|
-
gemini_api_key = Column(String, nullable=True)
|
|
60
56
|
parameters = Column(JSON, nullable=True)
|
|
61
57
|
created_at = Column(DateTime, default=datetime.now)
|
|
62
58
|
|
|
@@ -64,7 +60,7 @@ class Company(Base):
|
|
|
64
60
|
back_populates="company",
|
|
65
61
|
cascade="all, delete-orphan",
|
|
66
62
|
lazy='dynamic')
|
|
67
|
-
|
|
63
|
+
tools = relationship("Tool",
|
|
68
64
|
back_populates="company",
|
|
69
65
|
cascade="all, delete-orphan")
|
|
70
66
|
vsdocs = relationship("VSDoc",
|
|
@@ -80,13 +76,18 @@ class Company(Base):
|
|
|
80
76
|
back_populates="company",
|
|
81
77
|
cascade="all, delete-orphan")
|
|
82
78
|
|
|
83
|
-
tasks = relationship("Task", back_populates="company")
|
|
84
79
|
feedbacks = relationship("UserFeedback",
|
|
85
80
|
back_populates="company",
|
|
86
81
|
cascade="all, delete-orphan")
|
|
87
82
|
prompts = relationship("Prompt",
|
|
88
83
|
back_populates="company",
|
|
89
84
|
cascade="all, delete-orphan")
|
|
85
|
+
collection_types = relationship(
|
|
86
|
+
"CollectionType",
|
|
87
|
+
back_populates="company",
|
|
88
|
+
cascade="all, delete-orphan"
|
|
89
|
+
)
|
|
90
|
+
|
|
90
91
|
|
|
91
92
|
def to_dict(self):
|
|
92
93
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
@@ -96,14 +97,14 @@ class User(Base):
|
|
|
96
97
|
"""Represents an IAToolkit user who can be associated with multiple companies."""
|
|
97
98
|
__tablename__ = 'iat_users'
|
|
98
99
|
|
|
99
|
-
id = Column(Integer, primary_key=True)
|
|
100
|
-
email = Column(String
|
|
101
|
-
first_name = Column(String
|
|
102
|
-
last_name = Column(String
|
|
100
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
101
|
+
email = Column(String, unique=True, nullable=False)
|
|
102
|
+
first_name = Column(String, nullable=False)
|
|
103
|
+
last_name = Column(String, nullable=False)
|
|
103
104
|
created_at = Column(DateTime, default=datetime.now)
|
|
104
105
|
password = Column(String, nullable=False)
|
|
105
106
|
verified = Column(Boolean, nullable=False, default=False)
|
|
106
|
-
preferred_language = Column(String
|
|
107
|
+
preferred_language = Column(String, nullable=True)
|
|
107
108
|
verification_url = Column(String, nullable=True)
|
|
108
109
|
temp_code = Column(String, nullable=True)
|
|
109
110
|
|
|
@@ -127,27 +128,50 @@ class User(Base):
|
|
|
127
128
|
'companies': [company.to_dict() for company in self.companies]
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
class
|
|
131
|
+
class Tool(Base):
|
|
131
132
|
"""Represents a custom or system function that the LLM can call (tool)."""
|
|
132
|
-
__tablename__ = '
|
|
133
|
+
__tablename__ = 'iat_tools'
|
|
133
134
|
|
|
134
|
-
id = Column(Integer, primary_key=True)
|
|
135
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
135
136
|
company_id = Column(Integer,
|
|
136
137
|
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
137
138
|
nullable=True)
|
|
138
|
-
name = Column(String
|
|
139
|
+
name = Column(String, nullable=False)
|
|
139
140
|
system_function = Column(Boolean, default=False)
|
|
140
141
|
description = Column(Text, nullable=False)
|
|
141
142
|
parameters = Column(JSON, nullable=False)
|
|
142
143
|
is_active = Column(Boolean, default=True)
|
|
143
144
|
created_at = Column(DateTime, default=datetime.now)
|
|
144
145
|
|
|
145
|
-
company = relationship('Company', back_populates='
|
|
146
|
+
company = relationship('Company', back_populates='tools')
|
|
146
147
|
|
|
147
148
|
def to_dict(self):
|
|
148
149
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
class DocumentStatus(str, enum.Enum):
|
|
153
|
+
PENDING = "pending"
|
|
154
|
+
PROCESSING = "processing"
|
|
155
|
+
ACTIVE = "active"
|
|
156
|
+
FAILED = "failed"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CollectionType(Base):
|
|
160
|
+
"""Defines the available document collections/categories for a company."""
|
|
161
|
+
__tablename__ = 'iat_collection_types'
|
|
162
|
+
|
|
163
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
164
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
165
|
+
name = Column(String, nullable=False) # e.g., "Contracts", "Manuals"
|
|
166
|
+
|
|
167
|
+
# description - optional for the LLM to understand what's inside'
|
|
168
|
+
description = Column(Text, nullable=True)
|
|
169
|
+
|
|
170
|
+
__table_args__ = (UniqueConstraint('company_id', 'name', name='uix_company_collection_name'),)
|
|
171
|
+
|
|
172
|
+
company = relationship("Company", back_populates="collection_types")
|
|
173
|
+
documents = relationship("Document", back_populates="collection_type")
|
|
174
|
+
|
|
151
175
|
class Document(Base):
|
|
152
176
|
"""Represents a file or document uploaded by a company for context."""
|
|
153
177
|
__tablename__ = 'iat_documents'
|
|
@@ -155,27 +179,41 @@ class Document(Base):
|
|
|
155
179
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
156
180
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
157
181
|
ondelete='CASCADE'), nullable=False)
|
|
158
|
-
|
|
182
|
+
collection_type_id = Column(Integer, ForeignKey('iat_collection_types.id', ondelete='SET NULL'), nullable=True)
|
|
183
|
+
|
|
184
|
+
user_identifier = Column(String, nullable=True)
|
|
185
|
+
filename = Column(String, nullable=False, index=True)
|
|
186
|
+
status = Column(Enum(DocumentStatus), default=DocumentStatus.PENDING, nullable=False)
|
|
159
187
|
meta = Column(JSON, nullable=True)
|
|
160
188
|
created_at = Column(DateTime, default=datetime.now)
|
|
161
189
|
content = Column(Text, nullable=False)
|
|
162
190
|
content_b64 = Column(Text, nullable=False)
|
|
163
191
|
|
|
192
|
+
# For feedback if OCR or embedding fails
|
|
193
|
+
error_message = Column(Text, nullable=True)
|
|
194
|
+
|
|
195
|
+
# Hash column for deduplication (SHA-256 hex digest)
|
|
196
|
+
hash = Column(String(64), index=True, nullable=True)
|
|
197
|
+
|
|
164
198
|
company = relationship("Company", back_populates="documents")
|
|
199
|
+
collection_type = relationship("CollectionType", back_populates="documents")
|
|
165
200
|
|
|
166
201
|
def to_dict(self):
|
|
167
202
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
168
203
|
|
|
204
|
+
@property
|
|
205
|
+
def description(self):
|
|
206
|
+
collection_type = self.collection_type.name if self.collection_type else None
|
|
207
|
+
return f"Document ID {self.id}: {self.filename} ({collection_type})"
|
|
169
208
|
|
|
170
209
|
class LLMQuery(Base):
|
|
171
210
|
"""Logs a query made to the LLM, including input, output, and metadata."""
|
|
172
211
|
__tablename__ = 'iat_queries'
|
|
173
212
|
|
|
174
|
-
id = Column(Integer, primary_key=True)
|
|
213
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
175
214
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
176
215
|
ondelete='CASCADE'), nullable=False)
|
|
177
|
-
user_identifier = Column(String
|
|
178
|
-
task_id = Column(Integer, default=0, nullable=True)
|
|
216
|
+
user_identifier = Column(String, nullable=False)
|
|
179
217
|
query = Column(Text, nullable=False)
|
|
180
218
|
output = Column(Text, nullable=False)
|
|
181
219
|
response = Column(JSON, nullable=True, default={})
|
|
@@ -186,7 +224,6 @@ class LLMQuery(Base):
|
|
|
186
224
|
created_at = Column(DateTime, default=datetime.now)
|
|
187
225
|
|
|
188
226
|
company = relationship("Company", back_populates="llm_queries")
|
|
189
|
-
tasks = relationship("Task", back_populates="llm_query")
|
|
190
227
|
|
|
191
228
|
def to_dict(self):
|
|
192
229
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
@@ -196,7 +233,7 @@ class VSDoc(Base):
|
|
|
196
233
|
"""Stores a text chunk and its corresponding vector embedding for similarity search."""
|
|
197
234
|
__tablename__ = "iat_vsdocs"
|
|
198
235
|
|
|
199
|
-
id = Column(Integer, primary_key=True)
|
|
236
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
200
237
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
201
238
|
ondelete='CASCADE'), nullable=False)
|
|
202
239
|
document_id = Column(Integer, ForeignKey('iat_documents.id',
|
|
@@ -205,68 +242,22 @@ class VSDoc(Base):
|
|
|
205
242
|
|
|
206
243
|
# the size of this vector should be set depending on the embedding model used
|
|
207
244
|
# for OpenAI is 1536, and for huggingface is 384
|
|
208
|
-
embedding = Column(Vector(
|
|
245
|
+
embedding = Column(Vector(384), nullable=False)
|
|
209
246
|
|
|
210
247
|
company = relationship("Company", back_populates="vsdocs")
|
|
211
248
|
|
|
212
249
|
def to_dict(self):
|
|
213
250
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
214
251
|
|
|
215
|
-
class TaskStatus(PyEnum):
|
|
216
|
-
"""Enumeration for the possible statuses of a Task."""
|
|
217
|
-
pendiente = "pendiente" # task created and waiting to be executed.
|
|
218
|
-
ejecutado = "ejecutado" # the IA algorithm has been executed.
|
|
219
|
-
aprobada = "aprobada" # validated and approved by human.
|
|
220
|
-
rechazada = "rechazada" # validated and rejected by human.
|
|
221
|
-
fallida = "fallida" # error executing the IA algorithm.
|
|
222
|
-
|
|
223
|
-
class TaskType(Base):
|
|
224
|
-
"""Defines a type of task that can be executed, including its prompt template."""
|
|
225
|
-
__tablename__ = 'iat_task_types'
|
|
226
|
-
|
|
227
|
-
id = Column(Integer, primary_key=True)
|
|
228
|
-
name = Column(String(100), unique=True, nullable=False)
|
|
229
|
-
prompt_template = Column(String(100), nullable=True) # Plantilla de prompt por defecto.
|
|
230
|
-
template_args = Column(JSON, nullable=True) # Argumentos/prefijos de configuración para el template.
|
|
231
|
-
|
|
232
|
-
class Task(Base):
|
|
233
|
-
"""Represents an asynchronous task to be executed by the system, often involving an LLM."""
|
|
234
|
-
__tablename__ = 'iat_tasks'
|
|
235
|
-
|
|
236
|
-
id = Column(Integer, primary_key=True)
|
|
237
|
-
company_id = Column(Integer, ForeignKey("iat_companies.id"))
|
|
238
|
-
|
|
239
|
-
user_id = Column(Integer, nullable=True, default=0)
|
|
240
|
-
task_type_id = Column(Integer, ForeignKey('iat_task_types.id'), nullable=False)
|
|
241
|
-
status = Column(Enum(TaskStatus, name="task_status_enum"),
|
|
242
|
-
default=TaskStatus.pendiente, nullable=False)
|
|
243
|
-
client_data = Column(JSON, nullable=True, default={})
|
|
244
|
-
company_task_id = Column(Integer, nullable=True, default=0)
|
|
245
|
-
execute_at = Column(DateTime, default=datetime.now, nullable=True)
|
|
246
|
-
llm_query_id = Column(Integer, ForeignKey('iat_queries.id'), nullable=True)
|
|
247
|
-
callback_url = Column(String(512), default=None, nullable=True)
|
|
248
|
-
files = Column(JSON, default=[], nullable=True)
|
|
249
|
-
|
|
250
|
-
review_user = Column(String(128), nullable=True, default='')
|
|
251
|
-
review_date = Column(DateTime, nullable=True)
|
|
252
|
-
comment = Column(Text, nullable=True)
|
|
253
|
-
approved = Column(Boolean, nullable=False, default=False)
|
|
254
|
-
|
|
255
|
-
created_at = Column(DateTime, default=datetime.now)
|
|
256
|
-
updated_at = Column(DateTime, default=datetime.now)
|
|
257
|
-
|
|
258
|
-
task_type = relationship("TaskType")
|
|
259
|
-
llm_query = relationship("LLMQuery", back_populates="tasks", uselist=False)
|
|
260
|
-
company = relationship("Company", back_populates="tasks")
|
|
261
252
|
|
|
262
253
|
class UserFeedback(Base):
|
|
263
254
|
"""Stores feedback and ratings submitted by users for specific interactions."""
|
|
264
255
|
__tablename__ = 'iat_feedback'
|
|
265
256
|
|
|
266
|
-
id = Column(Integer, primary_key=True)
|
|
257
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
267
258
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
268
259
|
ondelete='CASCADE'), nullable=False)
|
|
269
|
-
user_identifier = Column(String
|
|
260
|
+
user_identifier = Column(String, default='', nullable=True)
|
|
270
261
|
message = Column(Text, nullable=False)
|
|
271
262
|
rating = Column(Integer, nullable=False)
|
|
272
263
|
created_at = Column(DateTime, default=datetime.now)
|
|
@@ -277,7 +268,7 @@ class UserFeedback(Base):
|
|
|
277
268
|
class PromptCategory(Base):
|
|
278
269
|
"""Represents a category to group and organize prompts."""
|
|
279
270
|
__tablename__ = 'iat_prompt_categories'
|
|
280
|
-
id = Column(Integer, primary_key=True)
|
|
271
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
281
272
|
name = Column(String, nullable=False)
|
|
282
273
|
order = Column(Integer, nullable=False, default=0)
|
|
283
274
|
company_id = Column(Integer, ForeignKey('iat_companies.id'), nullable=False)
|
|
@@ -292,15 +283,15 @@ class Prompt(Base):
|
|
|
292
283
|
"""Represents a system or user-defined prompt template for the LLM."""
|
|
293
284
|
__tablename__ = 'iat_prompt'
|
|
294
285
|
|
|
295
|
-
id = Column(Integer, primary_key=True)
|
|
286
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
296
287
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
297
288
|
ondelete='CASCADE'), nullable=True)
|
|
298
|
-
name = Column(String
|
|
299
|
-
description = Column(String
|
|
300
|
-
filename = Column(String
|
|
289
|
+
name = Column(String, nullable=False)
|
|
290
|
+
description = Column(String, nullable=False)
|
|
291
|
+
filename = Column(String, nullable=False)
|
|
301
292
|
active = Column(Boolean, default=True)
|
|
302
293
|
is_system_prompt = Column(Boolean, default=False)
|
|
303
|
-
order = Column(Integer, nullable=
|
|
294
|
+
order = Column(Integer, nullable=True, default=0)
|
|
304
295
|
category_id = Column(Integer, ForeignKey('iat_prompt_categories.id'), nullable=True)
|
|
305
296
|
custom_fields = Column(JSON, nullable=False, default=[])
|
|
306
297
|
|
|
@@ -313,21 +304,21 @@ class AccessLog(Base):
|
|
|
313
304
|
# Modelo ORM para registrar cada intento de acceso a la plataforma.
|
|
314
305
|
__tablename__ = 'iat_access_log'
|
|
315
306
|
|
|
316
|
-
id = Column(BigInteger, primary_key=True)
|
|
307
|
+
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
317
308
|
|
|
318
309
|
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
319
|
-
company_short_name = Column(String
|
|
320
|
-
user_identifier = Column(String
|
|
310
|
+
company_short_name = Column(String, nullable=False, index=True)
|
|
311
|
+
user_identifier = Column(String, index=True)
|
|
321
312
|
|
|
322
313
|
# Cómo y el Resultado
|
|
323
|
-
auth_type = Column(String
|
|
324
|
-
outcome = Column(String
|
|
325
|
-
reason_code = Column(String
|
|
314
|
+
auth_type = Column(String, nullable=False) # 'local', 'external_api', 'redeem_token', etc.
|
|
315
|
+
outcome = Column(String, nullable=False) # 'success' o 'failure'
|
|
316
|
+
reason_code = Column(String) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
|
|
326
317
|
|
|
327
318
|
# Contexto de la Petición
|
|
328
|
-
source_ip = Column(String
|
|
329
|
-
user_agent_hash = Column(String
|
|
330
|
-
request_path = Column(String
|
|
319
|
+
source_ip = Column(String, nullable=False)
|
|
320
|
+
user_agent_hash = Column(String) # Hash corto del User-Agent
|
|
321
|
+
request_path = Column(String, nullable=False)
|
|
331
322
|
|
|
332
323
|
def __repr__(self):
|
|
333
324
|
return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from iatoolkit.repositories.models import User, Company,
|
|
6
|
+
from iatoolkit.repositories.models import (User, Company, user_company,
|
|
7
|
+
ApiKey, UserFeedback, AccessLog)
|
|
7
8
|
from injector import inject
|
|
8
9
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
9
|
-
from sqlalchemy
|
|
10
|
+
from sqlalchemy import select, func, and_
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ProfileRepo:
|
|
@@ -69,8 +70,63 @@ class ProfileRepo:
|
|
|
69
70
|
def get_companies(self) -> list[Company]:
|
|
70
71
|
return self.session.query(Company).all()
|
|
71
72
|
|
|
73
|
+
def get_user_role_in_company(self, company_id, user_id, ):
|
|
74
|
+
stmt = (
|
|
75
|
+
select(user_company.c.role)
|
|
76
|
+
.where(
|
|
77
|
+
user_company.c.user_id == user_id,
|
|
78
|
+
user_company.c.company_id == company_id,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
result = self.session.execute(stmt).scalar_one_or_none()
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def get_companies_by_user_identifier(self, user_identifier: str) -> list:
|
|
85
|
+
"""
|
|
86
|
+
Return all the companies to which the user belongs (by email),
|
|
87
|
+
and the role he has in each company.
|
|
88
|
+
"""
|
|
89
|
+
return (
|
|
90
|
+
self.session.query(Company, user_company.c.role)
|
|
91
|
+
.join(user_company, Company.id == user_company.c.company_id)
|
|
92
|
+
.join(User, User.id == user_company.c.user_id)
|
|
93
|
+
.filter(User.email == user_identifier)
|
|
94
|
+
.all()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def get_company_users_with_details(self, company_short_name: str) -> list[dict]:
|
|
98
|
+
# returns the list of users in the company with their role and last access date
|
|
99
|
+
|
|
100
|
+
# subquery for last access date
|
|
101
|
+
last_access_sq = (
|
|
102
|
+
self.session.query(
|
|
103
|
+
AccessLog.user_identifier,
|
|
104
|
+
func.max(AccessLog.timestamp).label("max_ts")
|
|
105
|
+
)
|
|
106
|
+
.filter(AccessLog.company_short_name == company_short_name)
|
|
107
|
+
.group_by(AccessLog.user_identifier)
|
|
108
|
+
.subquery()
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# main query
|
|
112
|
+
stmt = (
|
|
113
|
+
self.session.query(
|
|
114
|
+
User,
|
|
115
|
+
user_company.c.role,
|
|
116
|
+
last_access_sq.c.max_ts
|
|
117
|
+
)
|
|
118
|
+
.join(user_company, User.id == user_company.c.user_id)
|
|
119
|
+
.join(Company, Company.id == user_company.c.company_id)
|
|
120
|
+
.outerjoin(last_access_sq, User.email == last_access_sq.c.user_identifier)
|
|
121
|
+
.filter(Company.short_name == company_short_name)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
results = stmt.all()
|
|
125
|
+
|
|
126
|
+
return results
|
|
127
|
+
|
|
72
128
|
def create_company(self, new_company: Company):
|
|
73
|
-
company = self.session.query(Company).filter_by(
|
|
129
|
+
company = self.session.query(Company).filter_by(short_name=new_company.short_name).first()
|
|
74
130
|
if company:
|
|
75
131
|
if company.parameters != new_company.parameters:
|
|
76
132
|
company.parameters = new_company.parameters
|
|
@@ -98,16 +154,11 @@ class ProfileRepo:
|
|
|
98
154
|
search for an active API Key by its value.
|
|
99
155
|
returns the entry if found and is active, None otherwise.
|
|
100
156
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.first()
|
|
107
|
-
return api_key_entry
|
|
108
|
-
except Exception:
|
|
109
|
-
self.session.rollback() # Asegura que la sesión esté limpia tras un error
|
|
110
|
-
return None
|
|
157
|
+
api_key_entry = (self.session.query(ApiKey).filter
|
|
158
|
+
(ApiKey.key == api_key_value, ApiKey.is_active == True).first())
|
|
159
|
+
|
|
160
|
+
return api_key_entry
|
|
161
|
+
|
|
111
162
|
|
|
112
163
|
def get_active_api_key_by_company(self, company: Company) -> ApiKey | None:
|
|
113
164
|
return self.session.query(ApiKey)\
|