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.

Files changed (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {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
+