iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__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.
Potentially problematic release.
This version of iatoolkit might be problematic. Click here for more details.
- iatoolkit/__init__.py +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -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/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +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 +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
|
@@ -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()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.models import LLMQuery, Tool, 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 commit(self):
|
|
17
|
+
self.session.commit()
|
|
18
|
+
|
|
19
|
+
def rollback(self):
|
|
20
|
+
self.session.rollback()
|
|
21
|
+
|
|
22
|
+
def add_query(self, query: LLMQuery):
|
|
23
|
+
self.session.add(query)
|
|
24
|
+
self.session.commit()
|
|
25
|
+
return query
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_company_tools(self, company: Company) -> list[Tool]:
|
|
29
|
+
return (
|
|
30
|
+
self.session.query(Tool)
|
|
31
|
+
.filter(
|
|
32
|
+
Tool.is_active.is_(True),
|
|
33
|
+
or_(
|
|
34
|
+
Tool.company_id == company.id,
|
|
35
|
+
Tool.system_function.is_(True)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
# Ordenamos descendente: True (System) va primero, False (Company) va después
|
|
39
|
+
.order_by(Tool.system_function.desc())
|
|
40
|
+
.all()
|
|
41
|
+
)
|
|
42
|
+
|
|
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
|
|
53
|
+
else:
|
|
54
|
+
self.session.add(new_tool)
|
|
55
|
+
tool = new_tool
|
|
56
|
+
|
|
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)
|
|
62
|
+
|
|
63
|
+
def create_or_update_prompt(self, new_prompt: Prompt):
|
|
64
|
+
prompt = self.session.query(Prompt).filter_by(company_id=new_prompt.company_id,
|
|
65
|
+
name=new_prompt.name).first()
|
|
66
|
+
if prompt:
|
|
67
|
+
prompt.category_id = new_prompt.category_id
|
|
68
|
+
prompt.description = new_prompt.description
|
|
69
|
+
prompt.order = new_prompt.order
|
|
70
|
+
prompt.is_system_prompt = new_prompt.is_system_prompt
|
|
71
|
+
prompt.filename = new_prompt.filename
|
|
72
|
+
prompt.custom_fields = new_prompt.custom_fields
|
|
73
|
+
else:
|
|
74
|
+
self.session.add(new_prompt)
|
|
75
|
+
prompt = new_prompt
|
|
76
|
+
|
|
77
|
+
self.session.flush()
|
|
78
|
+
return prompt
|
|
79
|
+
|
|
80
|
+
def create_or_update_prompt_category(self, new_category: PromptCategory):
|
|
81
|
+
category = self.session.query(PromptCategory).filter_by(company_id=new_category.company_id,
|
|
82
|
+
name=new_category.name).first()
|
|
83
|
+
if category:
|
|
84
|
+
category.order = new_category.order
|
|
85
|
+
else:
|
|
86
|
+
self.session.add(new_category)
|
|
87
|
+
category = new_category
|
|
88
|
+
|
|
89
|
+
self.session.flush()
|
|
90
|
+
return category
|
|
91
|
+
|
|
92
|
+
def get_history(self, company: Company, user_identifier: str) -> list[LLMQuery]:
|
|
93
|
+
return self.session.query(LLMQuery).filter(
|
|
94
|
+
LLMQuery.user_identifier == user_identifier,
|
|
95
|
+
).filter_by(company_id=company.id).order_by(LLMQuery.created_at.desc()).limit(100).all()
|
|
96
|
+
|
|
97
|
+
def get_prompts(self, company: Company) -> list[Prompt]:
|
|
98
|
+
return self.session.query(Prompt).filter_by(company_id=company.id, is_system_prompt=False).all()
|
|
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
|
+
|
|
103
|
+
def get_system_prompts(self) -> list[Prompt]:
|
|
104
|
+
return self.session.query(Prompt).filter_by(is_system_prompt=True, active=True).order_by(Prompt.order).all()
|
|
105
|
+
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
|
|
13
|
+
|
|
14
|
+
# base class for the ORM
|
|
15
|
+
class Base(DeclarativeBase):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
# relation table for many-to-many relationship between companies and users
|
|
19
|
+
user_company = Table('iat_user_company',
|
|
20
|
+
Base.metadata,
|
|
21
|
+
Column('user_id', Integer,
|
|
22
|
+
ForeignKey('iat_users.id', ondelete='CASCADE'),
|
|
23
|
+
primary_key=True),
|
|
24
|
+
Column('company_id', Integer,
|
|
25
|
+
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
26
|
+
primary_key=True),
|
|
27
|
+
Column('role', String, nullable=True, default='user'),
|
|
28
|
+
Column('created_at', DateTime, default=datetime.now)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
class ApiKey(Base):
|
|
32
|
+
"""Represents an API key for a company to authenticate against the system."""
|
|
33
|
+
__tablename__ = 'iat_api_keys'
|
|
34
|
+
|
|
35
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
36
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
37
|
+
key_name = Column(String, nullable=False)
|
|
38
|
+
key = Column(String, unique=True, nullable=False, index=True) # La API Key en sí
|
|
39
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
40
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
41
|
+
last_used_at = Column(DateTime, nullable=True) # Opcional: para rastrear uso
|
|
42
|
+
|
|
43
|
+
company = relationship("Company", back_populates="api_keys")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Company(Base):
|
|
47
|
+
"""Represents a company or tenant in the multi-tenant system."""
|
|
48
|
+
__tablename__ = 'iat_companies'
|
|
49
|
+
|
|
50
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
51
|
+
short_name = Column(String, nullable=False, unique=True, index=True)
|
|
52
|
+
name = Column(String, nullable=False)
|
|
53
|
+
|
|
54
|
+
parameters = Column(JSON, nullable=True)
|
|
55
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
56
|
+
|
|
57
|
+
documents = relationship("Document",
|
|
58
|
+
back_populates="company",
|
|
59
|
+
cascade="all, delete-orphan",
|
|
60
|
+
lazy='dynamic')
|
|
61
|
+
tools = relationship("Tool",
|
|
62
|
+
back_populates="company",
|
|
63
|
+
cascade="all, delete-orphan")
|
|
64
|
+
vsdocs = relationship("VSDoc",
|
|
65
|
+
back_populates="company",
|
|
66
|
+
cascade="all, delete-orphan")
|
|
67
|
+
llm_queries = relationship("LLMQuery",
|
|
68
|
+
back_populates="company",
|
|
69
|
+
cascade="all, delete-orphan")
|
|
70
|
+
users = relationship("User",
|
|
71
|
+
secondary=user_company,
|
|
72
|
+
back_populates="companies")
|
|
73
|
+
api_keys = relationship("ApiKey",
|
|
74
|
+
back_populates="company",
|
|
75
|
+
cascade="all, delete-orphan")
|
|
76
|
+
|
|
77
|
+
feedbacks = relationship("UserFeedback",
|
|
78
|
+
back_populates="company",
|
|
79
|
+
cascade="all, delete-orphan")
|
|
80
|
+
prompts = relationship("Prompt",
|
|
81
|
+
back_populates="company",
|
|
82
|
+
cascade="all, delete-orphan")
|
|
83
|
+
|
|
84
|
+
def to_dict(self):
|
|
85
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
86
|
+
|
|
87
|
+
# users with rights to use this app
|
|
88
|
+
class User(Base):
|
|
89
|
+
"""Represents an IAToolkit user who can be associated with multiple companies."""
|
|
90
|
+
__tablename__ = 'iat_users'
|
|
91
|
+
|
|
92
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
93
|
+
email = Column(String, unique=True, nullable=False)
|
|
94
|
+
first_name = Column(String, nullable=False)
|
|
95
|
+
last_name = Column(String, nullable=False)
|
|
96
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
97
|
+
password = Column(String, nullable=False)
|
|
98
|
+
verified = Column(Boolean, nullable=False, default=False)
|
|
99
|
+
preferred_language = Column(String, nullable=True)
|
|
100
|
+
verification_url = Column(String, nullable=True)
|
|
101
|
+
temp_code = Column(String, nullable=True)
|
|
102
|
+
|
|
103
|
+
companies = relationship(
|
|
104
|
+
"Company",
|
|
105
|
+
secondary=user_company,
|
|
106
|
+
back_populates="users",
|
|
107
|
+
cascade="all",
|
|
108
|
+
passive_deletes=True,
|
|
109
|
+
lazy='dynamic'
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def to_dict(self):
|
|
113
|
+
return {
|
|
114
|
+
'id': self.id,
|
|
115
|
+
'email': self.email,
|
|
116
|
+
'first_name': self.first_name,
|
|
117
|
+
'last_name': self.last_name,
|
|
118
|
+
'created_at': str(self.created_at),
|
|
119
|
+
'verified': self.verified,
|
|
120
|
+
'companies': [company.to_dict() for company in self.companies]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class Tool(Base):
|
|
124
|
+
"""Represents a custom or system function that the LLM can call (tool)."""
|
|
125
|
+
__tablename__ = 'iat_tools'
|
|
126
|
+
|
|
127
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
128
|
+
company_id = Column(Integer,
|
|
129
|
+
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
130
|
+
nullable=True)
|
|
131
|
+
name = Column(String, nullable=False)
|
|
132
|
+
system_function = Column(Boolean, default=False)
|
|
133
|
+
description = Column(Text, nullable=False)
|
|
134
|
+
parameters = Column(JSON, nullable=False)
|
|
135
|
+
is_active = Column(Boolean, default=True)
|
|
136
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
137
|
+
|
|
138
|
+
company = relationship('Company', back_populates='tools')
|
|
139
|
+
|
|
140
|
+
def to_dict(self):
|
|
141
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Document(Base):
|
|
145
|
+
"""Represents a file or document uploaded by a company for context."""
|
|
146
|
+
__tablename__ = 'iat_documents'
|
|
147
|
+
|
|
148
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
149
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
150
|
+
ondelete='CASCADE'), nullable=False)
|
|
151
|
+
filename = Column(String, nullable=False, index=True)
|
|
152
|
+
meta = Column(JSON, nullable=True)
|
|
153
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
154
|
+
content = Column(Text, nullable=False)
|
|
155
|
+
content_b64 = Column(Text, nullable=False)
|
|
156
|
+
|
|
157
|
+
company = relationship("Company", back_populates="documents")
|
|
158
|
+
|
|
159
|
+
def to_dict(self):
|
|
160
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LLMQuery(Base):
|
|
164
|
+
"""Logs a query made to the LLM, including input, output, and metadata."""
|
|
165
|
+
__tablename__ = 'iat_queries'
|
|
166
|
+
|
|
167
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
168
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
169
|
+
ondelete='CASCADE'), nullable=False)
|
|
170
|
+
user_identifier = Column(String, nullable=False)
|
|
171
|
+
query = Column(Text, nullable=False)
|
|
172
|
+
output = Column(Text, nullable=False)
|
|
173
|
+
response = Column(JSON, nullable=True, default={})
|
|
174
|
+
valid_response = Column(Boolean, nullable=False, default=False)
|
|
175
|
+
function_calls = Column(JSON, nullable=True, default={})
|
|
176
|
+
stats = Column(JSON, default={})
|
|
177
|
+
answer_time = Column(Integer, default=0)
|
|
178
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
179
|
+
|
|
180
|
+
company = relationship("Company", back_populates="llm_queries")
|
|
181
|
+
|
|
182
|
+
def to_dict(self):
|
|
183
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class VSDoc(Base):
|
|
187
|
+
"""Stores a text chunk and its corresponding vector embedding for similarity search."""
|
|
188
|
+
__tablename__ = "iat_vsdocs"
|
|
189
|
+
|
|
190
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
191
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
192
|
+
ondelete='CASCADE'), nullable=False)
|
|
193
|
+
document_id = Column(Integer, ForeignKey('iat_documents.id',
|
|
194
|
+
ondelete='CASCADE'), nullable=False)
|
|
195
|
+
text = Column(Text, nullable=False)
|
|
196
|
+
|
|
197
|
+
# the size of this vector should be set depending on the embedding model used
|
|
198
|
+
# for OpenAI is 1536, and for huggingface is 384
|
|
199
|
+
embedding = Column(Vector(1536), nullable=False)
|
|
200
|
+
|
|
201
|
+
company = relationship("Company", back_populates="vsdocs")
|
|
202
|
+
|
|
203
|
+
def to_dict(self):
|
|
204
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class UserFeedback(Base):
|
|
208
|
+
"""Stores feedback and ratings submitted by users for specific interactions."""
|
|
209
|
+
__tablename__ = 'iat_feedback'
|
|
210
|
+
|
|
211
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
212
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
213
|
+
ondelete='CASCADE'), nullable=False)
|
|
214
|
+
user_identifier = Column(String, default='', nullable=True)
|
|
215
|
+
message = Column(Text, nullable=False)
|
|
216
|
+
rating = Column(Integer, nullable=False)
|
|
217
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
218
|
+
|
|
219
|
+
company = relationship("Company", back_populates="feedbacks")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class PromptCategory(Base):
|
|
223
|
+
"""Represents a category to group and organize prompts."""
|
|
224
|
+
__tablename__ = 'iat_prompt_categories'
|
|
225
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
226
|
+
name = Column(String, nullable=False)
|
|
227
|
+
order = Column(Integer, nullable=False, default=0)
|
|
228
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id'), nullable=False)
|
|
229
|
+
|
|
230
|
+
prompts = relationship("Prompt", back_populates="category", order_by="Prompt.order")
|
|
231
|
+
|
|
232
|
+
def __repr__(self):
|
|
233
|
+
return f"<PromptCategory(name='{self.name}', order={self.order})>"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class Prompt(Base):
|
|
237
|
+
"""Represents a system or user-defined prompt template for the LLM."""
|
|
238
|
+
__tablename__ = 'iat_prompt'
|
|
239
|
+
|
|
240
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
241
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
242
|
+
ondelete='CASCADE'), nullable=True)
|
|
243
|
+
name = Column(String, nullable=False)
|
|
244
|
+
description = Column(String, nullable=False)
|
|
245
|
+
filename = Column(String, nullable=False)
|
|
246
|
+
active = Column(Boolean, default=True)
|
|
247
|
+
is_system_prompt = Column(Boolean, default=False)
|
|
248
|
+
order = Column(Integer, nullable=True, default=0)
|
|
249
|
+
category_id = Column(Integer, ForeignKey('iat_prompt_categories.id'), nullable=True)
|
|
250
|
+
custom_fields = Column(JSON, nullable=False, default=[])
|
|
251
|
+
|
|
252
|
+
created_at = Column(DateTime, default=datetime.now)
|
|
253
|
+
|
|
254
|
+
company = relationship("Company", back_populates="prompts")
|
|
255
|
+
category = relationship("PromptCategory", back_populates="prompts")
|
|
256
|
+
|
|
257
|
+
class AccessLog(Base):
|
|
258
|
+
# Modelo ORM para registrar cada intento de acceso a la plataforma.
|
|
259
|
+
__tablename__ = 'iat_access_log'
|
|
260
|
+
|
|
261
|
+
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
262
|
+
|
|
263
|
+
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
264
|
+
company_short_name = Column(String, nullable=False, index=True)
|
|
265
|
+
user_identifier = Column(String, index=True)
|
|
266
|
+
|
|
267
|
+
# Cómo y el Resultado
|
|
268
|
+
auth_type = Column(String, nullable=False) # 'local', 'external_api', 'redeem_token', etc.
|
|
269
|
+
outcome = Column(String, nullable=False) # 'success' o 'failure'
|
|
270
|
+
reason_code = Column(String) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
|
|
271
|
+
|
|
272
|
+
# Contexto de la Petición
|
|
273
|
+
source_ip = Column(String, nullable=False)
|
|
274
|
+
user_agent_hash = Column(String) # Hash corto del User-Agent
|
|
275
|
+
request_path = Column(String, nullable=False)
|
|
276
|
+
|
|
277
|
+
def __repr__(self):
|
|
278
|
+
return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
|
|
279
|
+
f"user='{self.user_identifier}', outcome='{self.outcome}')>")
|
|
@@ -0,0 +1,171 @@
|
|
|
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, user_company,
|
|
7
|
+
ApiKey, UserFeedback, AccessLog)
|
|
8
|
+
from injector import inject
|
|
9
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
10
|
+
from sqlalchemy import select, func, and_
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProfileRepo:
|
|
14
|
+
@inject
|
|
15
|
+
def __init__(self, db_manager: DatabaseManager):
|
|
16
|
+
self.session = db_manager.get_session()
|
|
17
|
+
|
|
18
|
+
def get_user_by_id(self, user_id: int) -> User:
|
|
19
|
+
user = self.session.query(User).filter_by(id=user_id).first()
|
|
20
|
+
return user
|
|
21
|
+
|
|
22
|
+
def get_user_by_email(self, email: str) -> User:
|
|
23
|
+
user = self.session.query(User).filter_by(email=email).first()
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
def create_user(self, new_user: User):
|
|
27
|
+
self.session.add(new_user)
|
|
28
|
+
self.session.commit()
|
|
29
|
+
return new_user
|
|
30
|
+
|
|
31
|
+
def save_user(self,existing_user: User):
|
|
32
|
+
self.session.add(existing_user)
|
|
33
|
+
self.session.commit()
|
|
34
|
+
return existing_user
|
|
35
|
+
|
|
36
|
+
def update_user(self, email, **kwargs):
|
|
37
|
+
user = self.session.query(User).filter_by(email=email).first()
|
|
38
|
+
if not user:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
# get the fields for update
|
|
42
|
+
for key, value in kwargs.items():
|
|
43
|
+
if hasattr(user, key): # Asegura que el campo existe en User
|
|
44
|
+
setattr(user, key, value)
|
|
45
|
+
|
|
46
|
+
self.session.commit()
|
|
47
|
+
return user # return updated object
|
|
48
|
+
|
|
49
|
+
def verify_user(self, email):
|
|
50
|
+
return self.update_user(email, verified=True)
|
|
51
|
+
|
|
52
|
+
def set_temp_code(self, email, temp_code):
|
|
53
|
+
return self.update_user(email, temp_code=temp_code)
|
|
54
|
+
|
|
55
|
+
def reset_temp_code(self, email):
|
|
56
|
+
return self.update_user(email, temp_code=None)
|
|
57
|
+
|
|
58
|
+
def update_password(self, email, hashed_password):
|
|
59
|
+
return self.update_user(email, password=hashed_password)
|
|
60
|
+
|
|
61
|
+
def get_company(self, name: str) -> Company:
|
|
62
|
+
return self.session.query(Company).filter_by(name=name).first()
|
|
63
|
+
|
|
64
|
+
def get_company_by_id(self, company_id: int) -> Company:
|
|
65
|
+
return self.session.query(Company).filter_by(id=company_id).first()
|
|
66
|
+
|
|
67
|
+
def get_company_by_short_name(self, short_name: str) -> Company:
|
|
68
|
+
return self.session.query(Company).filter(Company.short_name == short_name).first()
|
|
69
|
+
|
|
70
|
+
def get_companies(self) -> list[Company]:
|
|
71
|
+
return self.session.query(Company).all()
|
|
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_admin_companies_by_user_identifier(self, user_identifier: str) -> list[Company]:
|
|
85
|
+
"""
|
|
86
|
+
Return all the companies to which the user belongs (by email),
|
|
87
|
+
and where also he has admin role in the iat_user_company table.
|
|
88
|
+
"""
|
|
89
|
+
return (
|
|
90
|
+
self.session.query(Company)
|
|
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
|
+
.filter(user_company.c.role == "admin")
|
|
95
|
+
.all()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_company_users_with_details(self, company_short_name: str) -> list[dict]:
|
|
99
|
+
# returns the list of users in the company with their role and last access date
|
|
100
|
+
|
|
101
|
+
# subquery for last access date
|
|
102
|
+
last_access_sq = (
|
|
103
|
+
self.session.query(
|
|
104
|
+
AccessLog.user_identifier,
|
|
105
|
+
func.max(AccessLog.timestamp).label("max_ts")
|
|
106
|
+
)
|
|
107
|
+
.filter(AccessLog.company_short_name == company_short_name)
|
|
108
|
+
.group_by(AccessLog.user_identifier)
|
|
109
|
+
.subquery()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# main query
|
|
113
|
+
stmt = (
|
|
114
|
+
self.session.query(
|
|
115
|
+
User,
|
|
116
|
+
user_company.c.role,
|
|
117
|
+
last_access_sq.c.max_ts
|
|
118
|
+
)
|
|
119
|
+
.join(user_company, User.id == user_company.c.user_id)
|
|
120
|
+
.join(Company, Company.id == user_company.c.company_id)
|
|
121
|
+
.outerjoin(last_access_sq, User.email == last_access_sq.c.user_identifier)
|
|
122
|
+
.filter(Company.short_name == company_short_name)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
results = stmt.all()
|
|
126
|
+
|
|
127
|
+
return results
|
|
128
|
+
|
|
129
|
+
def create_company(self, new_company: Company):
|
|
130
|
+
company = self.session.query(Company).filter_by(short_name=new_company.short_name).first()
|
|
131
|
+
if company:
|
|
132
|
+
if company.parameters != new_company.parameters:
|
|
133
|
+
company.parameters = new_company.parameters
|
|
134
|
+
else:
|
|
135
|
+
# Si la compañía no existe, la añade a la sesión.
|
|
136
|
+
self.session.add(new_company)
|
|
137
|
+
company = new_company
|
|
138
|
+
|
|
139
|
+
self.session.commit()
|
|
140
|
+
return company
|
|
141
|
+
|
|
142
|
+
def save_feedback(self, feedback: UserFeedback):
|
|
143
|
+
self.session.add(feedback)
|
|
144
|
+
self.session.commit()
|
|
145
|
+
return feedback
|
|
146
|
+
|
|
147
|
+
def create_api_key(self, new_api_key: ApiKey):
|
|
148
|
+
self.session.add(new_api_key)
|
|
149
|
+
self.session.commit()
|
|
150
|
+
return new_api_key
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
|
|
154
|
+
"""
|
|
155
|
+
search for an active API Key by its value.
|
|
156
|
+
returns the entry if found and is active, None otherwise.
|
|
157
|
+
"""
|
|
158
|
+
api_key_entry = (self.session.query(ApiKey).filter
|
|
159
|
+
(ApiKey.key == api_key_value, ApiKey.is_active == True).first())
|
|
160
|
+
|
|
161
|
+
return api_key_entry
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_active_api_key_by_company(self, company: Company) -> ApiKey | None:
|
|
165
|
+
return self.session.query(ApiKey)\
|
|
166
|
+
.filter(ApiKey.company == company, ApiKey.is_active == True)\
|
|
167
|
+
.first()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|