iatoolkit 0.22.1__py3-none-any.whl → 0.50.0__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 (46) hide show
  1. iatoolkit/common/routes.py +31 -31
  2. iatoolkit/common/session_manager.py +0 -1
  3. iatoolkit/common/util.py +0 -21
  4. iatoolkit/iatoolkit.py +5 -18
  5. iatoolkit/infra/llm_client.py +3 -5
  6. iatoolkit/infra/redis_session_manager.py +48 -2
  7. iatoolkit/repositories/models.py +1 -2
  8. iatoolkit/services/auth_service.py +74 -0
  9. iatoolkit/services/dispatcher_service.py +12 -21
  10. iatoolkit/services/excel_service.py +15 -15
  11. iatoolkit/services/history_service.py +2 -11
  12. iatoolkit/services/profile_service.py +83 -25
  13. iatoolkit/services/query_service.py +132 -82
  14. iatoolkit/services/tasks_service.py +1 -1
  15. iatoolkit/services/user_feedback_service.py +3 -6
  16. iatoolkit/services/user_session_context_service.py +112 -54
  17. iatoolkit/static/js/chat_feedback.js +1 -1
  18. iatoolkit/static/js/chat_history.js +1 -5
  19. iatoolkit/static/js/chat_main.js +1 -1
  20. iatoolkit/static/styles/landing_page.css +62 -2
  21. iatoolkit/system_prompts/query_main.prompt +3 -12
  22. iatoolkit/templates/_login_widget.html +6 -8
  23. iatoolkit/templates/chat.html +78 -4
  24. iatoolkit/templates/error.html +1 -1
  25. iatoolkit/templates/index.html +38 -11
  26. iatoolkit/templates/{home.html → login_test.html} +11 -51
  27. iatoolkit/views/external_login_view.py +50 -111
  28. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +4 -4
  29. iatoolkit/views/history_api_view.py +52 -0
  30. iatoolkit/views/init_context_api_view.py +62 -0
  31. iatoolkit/views/llmquery_api_view.py +50 -0
  32. iatoolkit/views/llmquery_web_view.py +38 -0
  33. iatoolkit/views/{home_view.py → login_test_view.py} +2 -5
  34. iatoolkit/views/login_view.py +79 -56
  35. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +4 -4
  36. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -19
  37. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.0.dist-info}/METADATA +2 -2
  38. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.0.dist-info}/RECORD +40 -41
  39. iatoolkit/common/auth.py +0 -200
  40. iatoolkit/templates/login.html +0 -43
  41. iatoolkit/views/download_file_view.py +0 -58
  42. iatoolkit/views/history_view.py +0 -57
  43. iatoolkit/views/init_context_view.py +0 -35
  44. iatoolkit/views/llmquery_view.py +0 -65
  45. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.0.dist-info}/WHEEL +0 -0
  46. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.0.dist-info}/top_level.txt +0 -0
iatoolkit/common/auth.py DELETED
@@ -1,200 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from flask import redirect, url_for
7
- from iatoolkit.common.session_manager import SessionManager
8
- from datetime import datetime, timezone
9
- from injector import inject
10
- from iatoolkit.repositories.profile_repo import ProfileRepo
11
- from iatoolkit.services.jwt_service import JWTService
12
- import logging
13
- from flask import request
14
- from typing import Optional
15
-
16
- MAX_INACTIVITY_SECONDS = 60*30
17
-
18
- class IAuthentication:
19
- @inject
20
-
21
- def __init__(self,
22
- profile_repo: ProfileRepo,
23
- jwt_service: JWTService):
24
- self.profile_repo = profile_repo
25
- self.jwt_service = jwt_service
26
-
27
- def verify(self, company_short_name: str, body_external_user_id: str = None) -> dict:
28
- # authentication is in this orden: JWT, API Key, Sesión
29
- local_user_id = None
30
- company_id = None
31
- auth_method = None
32
- external_user_id = None # for JWT or API Key
33
-
34
- # 1. try auth via JWT
35
- jwt_company_id, jwt_external_user_id, jwt_error_info = self._authenticate_via_chat_jwt(company_short_name)
36
-
37
- if jwt_company_id is not None and jwt_external_user_id is not None:
38
- auth_method = "JWT"
39
- company_id = jwt_company_id
40
- external_user_id = jwt_external_user_id
41
- local_user_id = 0
42
- elif jwt_error_info is not None:
43
- # explicit error in JWT (inválido, expirado, etc.)
44
- logging.warning(f"Fallo de autenticación JWT: {jwt_error_info}")
45
- return {"error_message": "Fallo de autenticación JWT"}
46
- else:
47
- # 2. JWT not apply, try by API Key
48
- api_key_company_id, api_key_error_info = self._authenticate_via_api_key(company_short_name)
49
-
50
- if api_key_company_id is not None:
51
- auth_method = "API Key"
52
- company_id = api_key_company_id
53
- external_user_id = body_external_user_id # API Key usa external_user_id del body
54
- local_user_id = 0
55
- elif api_key_error_info is not None:
56
- # explicit error in API Key (inválida, incorrecta, error interno)
57
- logging.warning(f"Fallo de autenticación API Key: {api_key_error_info}")
58
- return {"error_message": api_key_error_info}
59
- else:
60
- # 3. no JWT and API Key auth, try by Session
61
- self.check_if_user_is_logged_in(company_short_name) # raise exception or redirect if not logged in
62
-
63
- # In case not logged in check_if_user_is_logged_in redirects to login page
64
- auth_method = "Session"
65
- local_user_id = SessionManager.get('user_id')
66
- company_id = SessionManager.get('company_id')
67
- external_user_id = ""
68
-
69
- if not company_id or not local_user_id:
70
- logging.error(
71
- f"Sesión válida para {company_short_name} pero falta company_id o user_id en SessionManager.")
72
- return {"error_message": "Fallo interno en la autenticación o no autenticado"}
73
-
74
- # last verification of authentication
75
- if company_id is None or auth_method is None or local_user_id is None:
76
- # this condition should never happen,
77
- logging.error(
78
- f"Fallo inesperado en la lógica de autenticación para {company_short_name}. Ningún método tuvo éxito o devolvió error.")
79
- return {"error_message": "Fallo interno en la autenticación o no autenticado"}
80
-
81
- return {
82
- 'success': True,
83
- "auth_method": auth_method,
84
- "company_id": company_id,
85
- "auth_method": auth_method,
86
- "local_user_id": local_user_id,
87
- "external_user_id": external_user_id
88
- }
89
-
90
- def _authenticate_via_api_key(self, company_short_name_from_url: str):
91
- """
92
- try to authenticate using an API Key from the header 'Authorization'.
93
- Retorna (company_id, None) en éxito.
94
- Retorna (None, error_message) en fallo.
95
- """
96
- api_key_header = request.headers.get('Authorization')
97
- api_key_value = None
98
-
99
- # extract the key
100
- if api_key_header and api_key_header.startswith('Bearer '):
101
- api_key_value = api_key_header.split('Bearer ')[1]
102
- else:
103
- # there is no key in the headers expected
104
- return None, None
105
-
106
- # validate the api-key using ProfileRepo
107
- try:
108
- api_key_entry = self.profile_repo.get_active_api_key_entry(api_key_value)
109
- if not api_key_entry:
110
- logging.warning(f"Intento de acceso con API Key inválida o inactiva: {api_key_value[:5]}...")
111
- return None, "API Key inválida o inactiva"
112
-
113
- # check that the key belongs to the company
114
- # api_key_entry.company already loaded by joinedload
115
- if not api_key_entry.company or api_key_entry.company.short_name != company_short_name_from_url:
116
- return None, f"API Key no es válida para la compañía {company_short_name_from_url}"
117
-
118
- # successfull auth by API Key
119
- company_id = api_key_entry.company_id
120
-
121
- return company_id, None
122
-
123
- except Exception as e:
124
- logging.exception(f"Error interno durante validación de API Key: {e}")
125
- return None, "Error interno del servidor al validar API Key"
126
-
127
- def _authenticate_via_chat_jwt(self, company_short_name_from_url: str) -> tuple[
128
- Optional[int], Optional[str], Optional[str]]:
129
- """
130
- authenticate using an JWT chat session in the del header 'X-Chat-Token'.
131
- Return (company_id, external_user_id, None) on exit
132
- Returns (None, None, error_message) on fail.
133
- """
134
- chat_jwt = request.headers.get('X-Chat-Token')
135
- if not chat_jwt:
136
- return None, None, None
137
-
138
- # open the jwt token and retrieve the payload
139
- jwt_payload = self.jwt_service.validate_chat_jwt(chat_jwt, company_short_name_from_url)
140
- if not jwt_payload:
141
- # validation fails (token expired, incorrect signature, company , etc.)
142
- # validate_chat_jwt logs the specific failure
143
- return None, None, "Token de chat expirado, debes reingresar al chat"
144
-
145
- # JWT is validated: extract the company_id and external_user_id
146
- company_id = jwt_payload.get('company_id')
147
- external_user_id = jwt_payload.get('external_user_id')
148
-
149
- # Sanity check aditional, should never happen
150
- if not isinstance(company_id, int) or not external_user_id:
151
- logging.error(
152
- f"LLMQuery: JWT payload incompleto tras validación exitosa. CompanyID: {company_id}, UserID: {external_user_id}")
153
- return None, None, "Token de chat con formato interno incorrecto"
154
-
155
- return company_id, external_user_id, None
156
-
157
- def check_if_user_is_logged_in(self, company_short_name: str):
158
- if not SessionManager.get('user'):
159
- if company_short_name:
160
- return redirect(url_for('login', company_short_name=company_short_name))
161
- else:
162
- return redirect(url_for('home'))
163
-
164
- if company_short_name != SessionManager.get('company_short_name'):
165
- return redirect(url_for('login', company_short_name=company_short_name))
166
-
167
- # check session timeout
168
- if not self.check_session_timeout():
169
- SessionManager.clear()
170
- return redirect(url_for('login', company_short_name=company_short_name))
171
-
172
- # update last_activity
173
- SessionManager.set('last_activity', datetime.now(timezone.utc).timestamp())
174
-
175
- def check_session_timeout(self):
176
- # get last activity from session manager
177
- last_activity = SessionManager.get('last_activity')
178
- if not last_activity:
179
- return False
180
-
181
- # Tiempo actual en timestamp
182
- current_time = datetime.now(timezone.utc).timestamp()
183
-
184
- # get inactivity duration
185
- inactivity_duration = current_time - last_activity
186
-
187
- # verify if inactivity duration is greater than MAX_INACTIVITY_SECONDS
188
- if inactivity_duration > MAX_INACTIVITY_SECONDS:
189
- # close session
190
- return False
191
-
192
- # update last activity timestamp
193
- SessionManager.set('last_activity', current_time)
194
-
195
- return True # session is active
196
-
197
-
198
-
199
-
200
-
@@ -1,43 +0,0 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}Inicio de Sesión{% endblock %}
4
-
5
- {% block content %}
6
- <div class="container d-flex justify-content-center align-items-center vh-100">
7
- <div class="col-11 col-md-6 col-lg-4 border rounded p-3 shadow-sm">
8
- <h4 class="text-muted fw-semibold text-start mb-3">Iniciar Sesión - {{ company.name }}
9
- </h4>
10
- <form action="{{ url_for('login', company_short_name=company_short_name) }}"
11
- method="post">
12
- <div class="mb-3">
13
- <label for="email" class="form-label text-muted">Correo Electrónico</label>
14
- <input type="email" id="email" name="email"
15
- class="form-control" required
16
- value="{{ form_data.email if form_data else '' }}">
17
- </div>
18
- <div class="mb-3">
19
- <label for="password" class="form-label">Contraseña</label>
20
- <input type="password" id="password" name="password"
21
- class="form-control" required
22
- value="{{ form_data.password if form_data else '' }}">
23
- </div>
24
- <button type="submit" class="btn btn-primary w-100">Iniciar Sesión</button>
25
-
26
- <p class="text-muted text-start mt-3" style="text-align: justify;">
27
- Ingresa tus credenciales para acceder a tu cuenta. Si olvidaste tu contraseña,
28
- puedes recuperarla fácilmente a través del siguiente enlace.
29
- </p>
30
- <div class="text-center mt-3">
31
- <a href="{{ url_for('signup', company_short_name=company_short_name) }}">
32
- ¿No tienes cuenta? Regístrate
33
- </a>
34
- </div>
35
- <div class="text-center mt-3">
36
- <a href="{{ url_for('forgot_password', company_short_name=company_short_name) }}">
37
- ¿Olvidaste tu contraseña?
38
- </a>
39
- </div>
40
- </form>
41
- </div>
42
- </div>
43
- {% endblock %}
@@ -1,58 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- import logging
7
- import os
8
-
9
- from flask import current_app, jsonify, send_from_directory
10
- from flask.views import MethodView
11
- from injector import inject
12
-
13
- from iatoolkit.common.auth import IAuthentication
14
- from iatoolkit.services.excel_service import ExcelService
15
- from iatoolkit.services.profile_service import ProfileService
16
-
17
-
18
- class DownloadFileView(MethodView):
19
- @inject
20
- def __init__(self, iauthentication: IAuthentication, profile_service: ProfileService, excel_service: ExcelService):
21
- self.iauthentication = iauthentication
22
- self.profile_service = profile_service
23
- self.excel_service = excel_service
24
-
25
- def get(self, company_short_name: str, external_user_id: str, filename: str):
26
- if not external_user_id:
27
- return jsonify({"error": "Falta external_user_id"}), 400
28
-
29
- iauth = self.iauthentication.verify(
30
- company_short_name,
31
- body_external_user_id=external_user_id
32
- )
33
- if not iauth.get("success"):
34
- return jsonify(iauth), 401
35
-
36
- company = self.profile_service.get_company_by_short_name(company_short_name)
37
- if not company:
38
- return jsonify({"error": "Empresa no encontrada"}), 404
39
-
40
- file_validation = self.excel_service.validate_file_access(filename)
41
- if file_validation:
42
- return file_validation
43
-
44
- temp_dir = os.path.join(current_app.root_path, 'static', 'temp')
45
-
46
- try:
47
- response = send_from_directory(
48
- temp_dir,
49
- filename,
50
- as_attachment=True,
51
- mimetype='application/octet-stream'
52
- )
53
- logging.info(f"Archivo descargado via API: {filename}")
54
- return response
55
- except Exception as e:
56
- logging.error(f"Error descargando archivo {filename}: {str(e)}")
57
- return jsonify({"error": "Error descargando archivo"}), 500
58
-
@@ -1,57 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from flask import request, jsonify, render_template
7
- from flask.views import MethodView
8
- from iatoolkit.services.history_service import HistoryService
9
- from iatoolkit.common.auth import IAuthentication
10
- from injector import inject
11
- import logging
12
-
13
-
14
- class HistoryView(MethodView):
15
- @inject
16
- def __init__(self,
17
- iauthentication: IAuthentication,
18
- history_service: HistoryService ):
19
- self.iauthentication = iauthentication
20
- self.history_service = history_service
21
-
22
- def post(self, company_short_name):
23
- try:
24
- data = request.get_json()
25
- except Exception:
26
- return jsonify({"error_message": "Cuerpo de la solicitud JSON inválido o faltante"}), 400
27
-
28
- if not data:
29
- return jsonify({"error_message": "Cuerpo de la solicitud JSON inválido o faltante"}), 400
30
-
31
- # get access credentials
32
- iaut = self.iauthentication.verify(company_short_name, data.get("external_user_id"))
33
- if not iaut.get("success"):
34
- return jsonify(iaut), 401
35
-
36
- external_user_id = data.get("external_user_id")
37
- local_user_id = iaut.get("local_user_id", 0)
38
-
39
- try:
40
- response = self.history_service.get_history(
41
- company_short_name=company_short_name,
42
- external_user_id=external_user_id,
43
- local_user_id=local_user_id
44
- )
45
-
46
- if "error" in response:
47
- return {'error_message': response["error"]}, 402
48
-
49
- return response, 200
50
- except Exception as e:
51
- logging.exception(
52
- f"Error inesperado al obtener el historial de consultas para company {company_short_name}: {e}")
53
- if local_user_id:
54
- return render_template("error.html",
55
- message="Ha ocurrido un error inesperado."), 500
56
- else:
57
- return jsonify({"error_message": str(e)}), 500
@@ -1,35 +0,0 @@
1
- from flask.views import MethodView
2
- from injector import inject
3
- from iatoolkit.common.auth import IAuthentication
4
- from iatoolkit.services.query_service import QueryService
5
- from flask import jsonify
6
- import logging
7
-
8
- class InitContextView(MethodView):
9
-
10
- @inject
11
- def __init__(self,
12
- iauthentication: IAuthentication,
13
- query_service: QueryService
14
- ):
15
- self.iauthentication = iauthentication
16
- self.query_service = query_service
17
-
18
- def get(self, company_short_name: str, external_user_id: str):
19
- # 1. get access credentials
20
- iaut = self.iauthentication.verify(company_short_name, external_user_id)
21
- if not iaut.get("success"):
22
- return jsonify(iaut), 401
23
-
24
- try:
25
- # initialize the context
26
- self.query_service.llm_init_context(
27
- company_short_name=company_short_name,
28
- external_user_id=external_user_id
29
- )
30
-
31
- return {'status': 'OK'}, 200
32
- except Exception as e:
33
- logging.exception(
34
- f"Error inesperado al inicializar el contexto durante el login para company {company_short_name}: {e}")
35
- return jsonify({"error_message": str(e)}), 500
@@ -1,65 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from flask import request, jsonify, render_template
7
- from flask.views import MethodView
8
- from iatoolkit.services.query_service import QueryService
9
- from iatoolkit.common.auth import IAuthentication
10
- from injector import inject
11
- import logging
12
-
13
-
14
- class LLMQueryView(MethodView):
15
- @inject
16
- def __init__(self,
17
- iauthentication: IAuthentication,
18
- query_service: QueryService,
19
- ):
20
- self.iauthentication = iauthentication
21
- self.query_service = query_service
22
-
23
- def post(self, company_short_name):
24
- data = request.get_json()
25
- if not data:
26
- return jsonify({"error_message": "Cuerpo de la solicitud JSON inválido o faltante"}), 400
27
-
28
- # get access credentials
29
- iaut = self.iauthentication.verify(company_short_name, data.get("external_user_id"))
30
- if not iaut.get("success"):
31
- return jsonify(iaut), 401
32
-
33
- company_id = iaut.get("company_id")
34
- external_user_id = iaut.get("external_user_id")
35
- local_user_id = iaut.get("local_user_id")
36
-
37
- # now check the form
38
- question = data.get("question")
39
- files = data.get("files", [])
40
- client_data = data.get("client_data", {})
41
- prompt_name = data.get("prompt_name")
42
- if not question and not prompt_name:
43
- return jsonify({"error_message": "Falta la consulta o el prompt_name"}), 400
44
-
45
- try:
46
- response = self.query_service.llm_query(
47
- company_short_name=company_short_name,
48
- external_user_id=external_user_id,
49
- local_user_id=local_user_id,
50
- question=question,
51
- prompt_name=prompt_name,
52
- client_data=client_data,
53
- files=files)
54
- if "error" in response:
55
- return {'error_message': response.get("error_message", '')}, 401
56
-
57
- return response, 200
58
- except Exception as e:
59
- logging.exception(
60
- f"Error inesperado al procesar llm_query para company {company_short_name}: {e}")
61
- if local_user_id:
62
- return render_template("error.html",
63
- message="Ha ocurrido un error inesperado."), 500
64
- else:
65
- return jsonify({"error_message": str(e)}), 500