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.
Files changed (114) hide show
  1. iatoolkit/__init__.py +19 -7
  2. iatoolkit/base_company.py +1 -71
  3. iatoolkit/cli_commands.py +9 -21
  4. iatoolkit/common/exceptions.py +2 -0
  5. iatoolkit/common/interfaces/__init__.py +0 -0
  6. iatoolkit/common/interfaces/asset_storage.py +34 -0
  7. iatoolkit/common/interfaces/database_provider.py +38 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +53 -32
  10. iatoolkit/common/util.py +17 -12
  11. iatoolkit/company_registry.py +55 -14
  12. iatoolkit/{iatoolkit.py → core.py} +102 -72
  13. iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
  14. iatoolkit/infra/llm_providers/__init__.py +0 -0
  15. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  16. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  17. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  18. iatoolkit/infra/llm_proxy.py +235 -134
  19. iatoolkit/infra/llm_response.py +5 -0
  20. iatoolkit/locales/en.yaml +134 -4
  21. iatoolkit/locales/es.yaml +293 -162
  22. iatoolkit/repositories/database_manager.py +92 -22
  23. iatoolkit/repositories/document_repo.py +7 -0
  24. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  25. iatoolkit/repositories/llm_query_repo.py +36 -22
  26. iatoolkit/repositories/models.py +86 -95
  27. iatoolkit/repositories/profile_repo.py +64 -13
  28. iatoolkit/repositories/vs_repo.py +31 -28
  29. iatoolkit/services/auth_service.py +1 -1
  30. iatoolkit/services/branding_service.py +1 -1
  31. iatoolkit/services/company_context_service.py +96 -39
  32. iatoolkit/services/configuration_service.py +329 -67
  33. iatoolkit/services/dispatcher_service.py +51 -227
  34. iatoolkit/services/document_service.py +10 -1
  35. iatoolkit/services/embedding_service.py +9 -6
  36. iatoolkit/services/excel_service.py +50 -2
  37. iatoolkit/services/file_processor_service.py +0 -5
  38. iatoolkit/services/history_manager_service.py +208 -0
  39. iatoolkit/services/jwt_service.py +1 -1
  40. iatoolkit/services/knowledge_base_service.py +412 -0
  41. iatoolkit/services/language_service.py +8 -2
  42. iatoolkit/services/license_service.py +82 -0
  43. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +42 -29
  44. iatoolkit/services/load_documents_service.py +18 -47
  45. iatoolkit/services/mail_service.py +171 -25
  46. iatoolkit/services/profile_service.py +69 -36
  47. iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +136 -25
  48. iatoolkit/services/query_service.py +229 -203
  49. iatoolkit/services/sql_service.py +116 -34
  50. iatoolkit/services/tool_service.py +246 -0
  51. iatoolkit/services/user_feedback_service.py +18 -6
  52. iatoolkit/services/user_session_context_service.py +121 -51
  53. iatoolkit/static/images/iatoolkit_core.png +0 -0
  54. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  55. iatoolkit/static/js/chat_feedback_button.js +1 -1
  56. iatoolkit/static/js/chat_help_content.js +4 -4
  57. iatoolkit/static/js/chat_main.js +61 -9
  58. iatoolkit/static/js/chat_model_selector.js +227 -0
  59. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  60. iatoolkit/static/js/chat_reload_button.js +4 -1
  61. iatoolkit/static/styles/chat_iatoolkit.css +59 -3
  62. iatoolkit/static/styles/chat_public.css +28 -0
  63. iatoolkit/static/styles/documents.css +598 -0
  64. iatoolkit/static/styles/landing_page.css +223 -7
  65. iatoolkit/static/styles/llm_output.css +34 -1
  66. iatoolkit/system_prompts/__init__.py +0 -0
  67. iatoolkit/system_prompts/query_main.prompt +28 -3
  68. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  69. iatoolkit/templates/_company_header.html +30 -5
  70. iatoolkit/templates/_login_widget.html +3 -3
  71. iatoolkit/templates/base.html +13 -0
  72. iatoolkit/templates/chat.html +45 -3
  73. iatoolkit/templates/forgot_password.html +3 -2
  74. iatoolkit/templates/onboarding_shell.html +1 -2
  75. iatoolkit/templates/signup.html +3 -0
  76. iatoolkit/views/base_login_view.py +8 -3
  77. iatoolkit/views/change_password_view.py +1 -1
  78. iatoolkit/views/chat_view.py +76 -0
  79. iatoolkit/views/forgot_password_view.py +9 -4
  80. iatoolkit/views/history_api_view.py +3 -3
  81. iatoolkit/views/home_view.py +4 -2
  82. iatoolkit/views/init_context_api_view.py +1 -1
  83. iatoolkit/views/llmquery_api_view.py +4 -3
  84. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  85. iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +15 -11
  86. iatoolkit/views/login_view.py +25 -8
  87. iatoolkit/views/logout_api_view.py +10 -2
  88. iatoolkit/views/prompt_api_view.py +1 -1
  89. iatoolkit/views/rag_api_view.py +216 -0
  90. iatoolkit/views/root_redirect_view.py +22 -0
  91. iatoolkit/views/signup_view.py +12 -4
  92. iatoolkit/views/static_page_view.py +27 -0
  93. iatoolkit/views/users_api_view.py +33 -0
  94. iatoolkit/views/verify_user_view.py +1 -1
  95. iatoolkit-1.4.2.dist-info/METADATA +268 -0
  96. iatoolkit-1.4.2.dist-info/RECORD +133 -0
  97. iatoolkit-1.4.2.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  98. iatoolkit/repositories/tasks_repo.py +0 -52
  99. iatoolkit/services/history_service.py +0 -37
  100. iatoolkit/services/search_service.py +0 -55
  101. iatoolkit/services/tasks_service.py +0 -188
  102. iatoolkit/templates/about.html +0 -13
  103. iatoolkit/templates/index.html +0 -145
  104. iatoolkit/templates/login_simulation.html +0 -45
  105. iatoolkit/views/external_login_view.py +0 -73
  106. iatoolkit/views/index_view.py +0 -14
  107. iatoolkit/views/login_simulation_view.py +0 -93
  108. iatoolkit/views/tasks_api_view.py +0 -72
  109. iatoolkit/views/tasks_review_api_view.py +0 -55
  110. iatoolkit-0.71.4.dist-info/METADATA +0 -276
  111. iatoolkit-0.71.4.dist-info/RECORD +0 -122
  112. {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
  113. {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
  114. {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, Function, Company, Prompt, PromptCategory
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 get_company_functions(self, company: Company) -> list[Function]:
28
+ def get_company_tools(self, company: Company) -> list[Tool]:
23
29
  return (
24
- self.session.query(Function)
30
+ self.session.query(Tool)
25
31
  .filter(
26
- Function.is_active.is_(True),
32
+ Tool.is_active.is_(True),
27
33
  or_(
28
- Function.company_id == company.id,
29
- Function.system_function.is_(True)
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 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
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(new_function)
44
- function = new_function
54
+ self.session.add(new_tool)
55
+ tool = new_tool
45
56
 
46
- self.session.commit()
47
- return function
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.commit()
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.commit()
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()
@@ -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, declarative_base
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('is_active', Boolean, default=True),
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
- key = Column(String(128), unique=True, nullable=False, index=True) # La API Key en sí
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(20), nullable=False, unique=True, index=True)
55
- name = Column(String(256), nullable=False)
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
- functions = relationship("Function",
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(80), unique=True, nullable=False)
101
- first_name = Column(String(50), nullable=False)
102
- last_name = Column(String(50), nullable=False)
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(5), nullable=True)
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 Function(Base):
131
+ class Tool(Base):
131
132
  """Represents a custom or system function that the LLM can call (tool)."""
132
- __tablename__ = 'iat_functions'
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(255), nullable=False)
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='functions')
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
- filename = Column(String(256), nullable=False, index=True)
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(128), nullable=False)
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(1536), nullable=False)
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(128), default='', nullable=True)
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(64), nullable=False)
299
- description = Column(String(256), nullable=False)
300
- filename = Column(String(256), nullable=False)
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=False, default=0) # Nuevo campo para el orden
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(100), nullable=False, index=True)
320
- user_identifier = Column(String(255), index=True)
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(20), nullable=False) # 'local', 'external_api', 'redeem_token', etc.
324
- outcome = Column(String(10), nullable=False) # 'success' o 'failure'
325
- reason_code = Column(String(50)) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
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(45), nullable=False)
329
- user_agent_hash = Column(String(16)) # Hash corto del User-Agent
330
- request_path = Column(String(255), nullable=False)
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, ApiKey, UserFeedback
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.orm import joinedload # Para cargar la relación eficientemente
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(name=new_company.name).first()
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
- try:
102
- # Usamos joinedload para cargar la compañía en la misma consulta
103
- api_key_entry = self.session.query(ApiKey)\
104
- .options(joinedload(ApiKey.company))\
105
- .filter(ApiKey.key == api_key_value, ApiKey.is_active == True)\
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)\