iatoolkit 0.4.2__py3-none-any.whl → 0.66.2__py3-none-any.whl

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