iatoolkit 0.8.1__py3-none-any.whl → 0.63.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 (159) hide show
  1. iatoolkit/__init__.py +8 -34
  2. iatoolkit/base_company.py +14 -3
  3. iatoolkit/common/routes.py +83 -52
  4. iatoolkit/common/session_manager.py +0 -1
  5. iatoolkit/common/util.py +0 -27
  6. iatoolkit/iatoolkit.py +61 -46
  7. iatoolkit/infra/llm_client.py +7 -8
  8. iatoolkit/infra/openai_adapter.py +1 -1
  9. iatoolkit/infra/redis_session_manager.py +48 -2
  10. iatoolkit/repositories/database_manager.py +17 -2
  11. iatoolkit/repositories/models.py +31 -6
  12. iatoolkit/repositories/profile_repo.py +7 -2
  13. iatoolkit/services/auth_service.py +188 -0
  14. iatoolkit/services/branding_service.py +147 -0
  15. iatoolkit/services/dispatcher_service.py +10 -40
  16. iatoolkit/services/excel_service.py +15 -15
  17. iatoolkit/services/history_service.py +3 -12
  18. iatoolkit/services/jwt_service.py +15 -24
  19. iatoolkit/services/onboarding_service.py +43 -0
  20. iatoolkit/services/profile_service.py +97 -44
  21. iatoolkit/services/query_service.py +124 -81
  22. iatoolkit/services/tasks_service.py +1 -1
  23. iatoolkit/services/user_feedback_service.py +67 -31
  24. iatoolkit/services/user_session_context_service.py +112 -54
  25. iatoolkit/static/images/fernando.jpeg +0 -0
  26. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
  27. iatoolkit/static/js/chat_history_button.js +126 -0
  28. iatoolkit/static/js/chat_logout_button.js +36 -0
  29. iatoolkit/static/js/chat_main.js +130 -220
  30. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  31. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  32. iatoolkit/static/js/chat_reload_button.js +52 -0
  33. iatoolkit/static/styles/chat_iatoolkit.css +329 -507
  34. iatoolkit/static/styles/chat_modal.css +95 -56
  35. iatoolkit/static/styles/landing_page.css +182 -0
  36. iatoolkit/static/styles/onboarding.css +169 -0
  37. iatoolkit/system_prompts/query_main.prompt +3 -12
  38. iatoolkit/templates/_company_header.html +20 -0
  39. iatoolkit/templates/_login_widget.html +40 -0
  40. iatoolkit/templates/base.html +8 -3
  41. iatoolkit/templates/change_password.html +54 -37
  42. iatoolkit/templates/chat.html +149 -66
  43. iatoolkit/templates/chat_modals.html +47 -18
  44. iatoolkit/templates/error.html +41 -8
  45. iatoolkit/templates/forgot_password.html +37 -24
  46. iatoolkit/templates/index.html +140 -0
  47. iatoolkit/templates/login_simulation.html +34 -0
  48. iatoolkit/templates/onboarding_shell.html +105 -0
  49. iatoolkit/templates/signup.html +64 -66
  50. iatoolkit/views/base_login_view.py +81 -0
  51. iatoolkit/views/change_password_view.py +23 -12
  52. iatoolkit/views/external_login_view.py +61 -28
  53. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  54. iatoolkit/views/forgot_password_view.py +23 -13
  55. iatoolkit/views/history_api_view.py +52 -0
  56. iatoolkit/views/home_view.py +58 -25
  57. iatoolkit/views/index_view.py +14 -0
  58. iatoolkit/views/init_context_api_view.py +68 -0
  59. iatoolkit/views/llmquery_api_view.py +45 -0
  60. iatoolkit/views/login_simulation_view.py +81 -0
  61. iatoolkit/views/login_view.py +118 -34
  62. iatoolkit/views/logout_api_view.py +45 -0
  63. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
  64. iatoolkit/views/signup_view.py +38 -29
  65. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  66. iatoolkit/views/tasks_review_api_view.py +55 -0
  67. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
  68. iatoolkit/views/verify_user_view.py +13 -8
  69. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
  70. iatoolkit-0.63.4.dist-info/RECORD +113 -0
  71. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
  72. iatoolkit/common/auth.py +0 -200
  73. iatoolkit/static/images/arrow_up.png +0 -0
  74. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  75. iatoolkit/static/images/logo_clinica.png +0 -0
  76. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  77. iatoolkit/static/images/logo_maxxa.png +0 -0
  78. iatoolkit/static/images/logo_notaria.png +0 -0
  79. iatoolkit/static/images/logo_tarjeta.png +0 -0
  80. iatoolkit/static/images/logo_umayor.png +0 -0
  81. iatoolkit/static/images/upload.png +0 -0
  82. iatoolkit/static/js/chat_history.js +0 -117
  83. iatoolkit/templates/home.html +0 -201
  84. iatoolkit/templates/login.html +0 -43
  85. iatoolkit/views/chat_token_request_view.py +0 -98
  86. iatoolkit/views/chat_view.py +0 -51
  87. iatoolkit/views/download_file_view.py +0 -58
  88. iatoolkit/views/external_chat_login_view.py +0 -88
  89. iatoolkit/views/history_view.py +0 -57
  90. iatoolkit/views/llmquery_view.py +0 -65
  91. iatoolkit/views/tasks_review_view.py +0 -83
  92. iatoolkit-0.8.1.dist-info/RECORD +0 -175
  93. tests/__init__.py +0 -5
  94. tests/common/__init__.py +0 -0
  95. tests/common/test_auth.py +0 -279
  96. tests/common/test_routes.py +0 -42
  97. tests/common/test_session_manager.py +0 -59
  98. tests/common/test_util.py +0 -444
  99. tests/companies/__init__.py +0 -5
  100. tests/conftest.py +0 -36
  101. tests/infra/__init__.py +0 -5
  102. tests/infra/connectors/__init__.py +0 -5
  103. tests/infra/connectors/test_google_drive_connector.py +0 -107
  104. tests/infra/connectors/test_local_file_connector.py +0 -85
  105. tests/infra/connectors/test_s3_connector.py +0 -95
  106. tests/infra/test_call_service.py +0 -92
  107. tests/infra/test_database_manager.py +0 -59
  108. tests/infra/test_gemini_adapter.py +0 -137
  109. tests/infra/test_google_chat_app.py +0 -68
  110. tests/infra/test_llm_client.py +0 -165
  111. tests/infra/test_llm_proxy.py +0 -122
  112. tests/infra/test_mail_app.py +0 -94
  113. tests/infra/test_openai_adapter.py +0 -105
  114. tests/infra/test_redis_session_manager_service.py +0 -117
  115. tests/repositories/__init__.py +0 -5
  116. tests/repositories/test_database_manager.py +0 -87
  117. tests/repositories/test_document_repo.py +0 -76
  118. tests/repositories/test_llm_query_repo.py +0 -340
  119. tests/repositories/test_models.py +0 -38
  120. tests/repositories/test_profile_repo.py +0 -142
  121. tests/repositories/test_tasks_repo.py +0 -76
  122. tests/repositories/test_vs_repo.py +0 -107
  123. tests/services/__init__.py +0 -5
  124. tests/services/test_dispatcher_service.py +0 -274
  125. tests/services/test_document_service.py +0 -181
  126. tests/services/test_excel_service.py +0 -208
  127. tests/services/test_file_processor_service.py +0 -121
  128. tests/services/test_history_service.py +0 -164
  129. tests/services/test_jwt_service.py +0 -255
  130. tests/services/test_load_documents_service.py +0 -112
  131. tests/services/test_mail_service.py +0 -70
  132. tests/services/test_profile_service.py +0 -379
  133. tests/services/test_prompt_manager_service.py +0 -190
  134. tests/services/test_query_service.py +0 -243
  135. tests/services/test_search_service.py +0 -39
  136. tests/services/test_sql_service.py +0 -160
  137. tests/services/test_tasks_service.py +0 -252
  138. tests/services/test_user_feedback_service.py +0 -389
  139. tests/services/test_user_session_context_service.py +0 -132
  140. tests/views/__init__.py +0 -5
  141. tests/views/test_change_password_view.py +0 -191
  142. tests/views/test_chat_token_request_view.py +0 -188
  143. tests/views/test_chat_view.py +0 -98
  144. tests/views/test_download_file_view.py +0 -149
  145. tests/views/test_external_chat_login_view.py +0 -120
  146. tests/views/test_external_login_view.py +0 -102
  147. tests/views/test_file_store_view.py +0 -128
  148. tests/views/test_forgot_password_view.py +0 -142
  149. tests/views/test_history_view.py +0 -336
  150. tests/views/test_home_view.py +0 -61
  151. tests/views/test_llm_query_view.py +0 -154
  152. tests/views/test_login_view.py +0 -114
  153. tests/views/test_prompt_view.py +0 -111
  154. tests/views/test_signup_view.py +0 -140
  155. tests/views/test_tasks_review_view.py +0 -104
  156. tests/views/test_tasks_view.py +0 -130
  157. tests/views/test_user_feedback_view.py +0 -214
  158. tests/views/test_verify_user_view.py +0 -110
  159. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
@@ -3,38 +3,71 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from flask.views import MethodView
7
- from injector import inject
8
- from iatoolkit.common.auth import IAuthentication
9
- from iatoolkit.services.query_service import QueryService
10
- from flask import jsonify
6
+ import os
11
7
  import logging
8
+ from flask import request, jsonify, url_for
9
+ from iatoolkit.views.base_login_view import BaseLoginView
12
10
 
13
- class ExternalLoginView(MethodView):
14
11
 
15
- @inject
16
- def __init__(self,
17
- iauthentication: IAuthentication,
18
- query_service: QueryService
19
- ):
20
- self.iauthentication = iauthentication
21
- self.query_service = query_service
12
+ class ExternalLoginView(BaseLoginView):
13
+ """
14
+ Handles login for external users via API.
15
+ Authenticates and then delegates the path decision (fast/slow) to the base class.
16
+ """
17
+ def post(self, company_short_name: str):
18
+ # Authenticate the API call.
19
+ auth_result = self.auth_service.verify()
20
+ if not auth_result.get("success"):
21
+ return jsonify(auth_result), auth_result.get("status_code")
22
22
 
23
- def get(self, company_short_name: str, external_user_id: str):
24
- # 1. get access credentials
25
- iaut = self.iauthentication.verify(company_short_name, external_user_id)
26
- if not iaut.get("success"):
27
- return jsonify(iaut), 401
23
+ company = self.profile_service.get_company_by_short_name(company_short_name)
24
+ if not company:
25
+ return jsonify({"error": "Empresa no encontrada"}), 404
28
26
 
29
- try:
30
- # initialize the context
31
- self.query_service.llm_init_context(
32
- company_short_name=company_short_name,
33
- external_user_id=external_user_id
34
- )
27
+ user_identifier = auth_result.get('user_identifier')
28
+
29
+ # 2. Create the external user session.
30
+ self.profile_service.create_external_user_profile_context(company, user_identifier)
31
+
32
+ # 3. create a redeem_token for create session at the end of the process
33
+ redeem_token = self.jwt_service.generate_chat_jwt(
34
+ company_short_name=company_short_name,
35
+ user_identifier=user_identifier,
36
+ expires_delta_seconds=300
37
+ )
35
38
 
36
- return {'status': 'OK'}, 200
39
+ if not redeem_token:
40
+ return jsonify({"error": "Error al generar el redeem_token para login externo."}), 403
41
+
42
+ # 4. define URL to call when slow path is finished
43
+ target_url = url_for('finalize_with_token',
44
+ company_short_name=company_short_name,
45
+ token=redeem_token,
46
+ _external=True)
47
+
48
+ # 5. Delegate the path decision to the centralized logic.
49
+ try:
50
+ return self._handle_login_path(company, user_identifier, target_url, redeem_token)
37
51
  except Exception as e:
38
- logging.exception(
39
- f"Error inesperado al inicializar el contexto durante el login para company {company_short_name}: {e}")
40
- return jsonify({"error_message": str(e)}), 500
52
+ logging.exception(f"Error processing external login path for {company_short_name}/{user_identifier}: {e}")
53
+ return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
54
+
55
+
56
+ class RedeemTokenApiView(BaseLoginView):
57
+ # this endpoint is only used ONLY by chat_main.js to redeem a chat token
58
+ def post(self, company_short_name: str):
59
+ data = request.get_json()
60
+ if not data or 'token' not in data:
61
+ return jsonify({"error": "Falta token de validación"}), 400
62
+
63
+ # get the token and validate with auth service
64
+ token = data.get('token')
65
+ redeem_result = self.auth_service.redeem_token_for_session(
66
+ company_short_name=company_short_name,
67
+ token=token
68
+ )
69
+
70
+ if not redeem_result['success']:
71
+ return {"error": redeem_result['error']}, 401
72
+
73
+ return {"status": "ok"}, 200
@@ -6,23 +6,30 @@
6
6
  from flask.views import MethodView
7
7
  from flask import request, jsonify
8
8
  from iatoolkit.services.load_documents_service import LoadDocumentsService
9
+ from iatoolkit.services.auth_service import AuthService
9
10
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
11
  from injector import inject
11
12
  import base64
12
13
 
13
14
 
14
- class FileStoreView(MethodView):
15
+ class FileStoreApiView(MethodView):
15
16
  @inject
16
17
  def __init__(self,
18
+ auth_service: AuthService,
17
19
  doc_service: LoadDocumentsService,
18
20
  profile_repo: ProfileRepo,):
21
+ self.auth_service = auth_service
19
22
  self.doc_service = doc_service
20
23
  self.profile_repo = profile_repo
21
24
 
22
25
  def post(self):
23
26
  try:
24
- req_data = request.get_json()
27
+ # 1. Authenticate the API request.
28
+ auth_result = self.auth_service.verify()
29
+ if not auth_result.get("success"):
30
+ return jsonify(auth_result), auth_result.get("status_code")
25
31
 
32
+ req_data = request.get_json()
26
33
  required_fields = ['company', 'filename', 'content']
27
34
  for field in required_fields:
28
35
  if field not in req_data:
@@ -4,33 +4,43 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from flask.views import MethodView
7
- from flask import render_template, request, url_for
7
+ from flask import render_template, request, url_for, redirect, session
8
8
  from injector import inject
9
9
  from iatoolkit.services.profile_service import ProfileService
10
+ from iatoolkit.services.branding_service import BrandingService
10
11
  from itsdangerous import URLSafeTimedSerializer
11
12
  import os
12
13
 
13
14
  class ForgotPasswordView(MethodView):
14
15
  @inject
15
- def __init__(self, profile_service: ProfileService):
16
+ def __init__(self, profile_service: ProfileService,
17
+ branding_service: BrandingService):
16
18
  self.profile_service = profile_service
19
+ self.branding_service = branding_service # 3. Guardar la instancia
17
20
  self.serializer = URLSafeTimedSerializer(os.getenv("PASS_RESET_KEY"))
18
21
 
19
22
  def get(self, company_short_name: str):
20
23
  # get company info
21
24
  company = self.profile_service.get_company_by_short_name(company_short_name)
22
25
  if not company:
23
- return render_template('error.html', message="Empresa no encontrada"), 404
26
+ return render_template('error.html',
27
+ company_short_name=company_short_name,
28
+ message="Empresa no encontrada"), 404
24
29
 
30
+ branding_data = self.branding_service.get_company_branding(company)
25
31
  return render_template('forgot_password.html',
26
32
  company=company,
27
- company_short_name=company_short_name
33
+ company_short_name=company_short_name,
34
+ branding=branding_data
28
35
  )
29
36
 
30
37
  def post(self, company_short_name: str):
31
38
  company = self.profile_service.get_company_by_short_name(company_short_name)
32
39
  if not company:
33
- return render_template('error.html', message="Empresa no encontrada"), 404
40
+ return render_template('error.html',
41
+ company_short_name=company_short_name,
42
+ message="Empresa no encontrada"), 404
43
+ branding_data = self.branding_service.get_company_branding(company)
34
44
 
35
45
  try:
36
46
  email = request.form.get('email')
@@ -47,18 +57,18 @@ class ForgotPasswordView(MethodView):
47
57
  'forgot_password.html',
48
58
  company=company,
49
59
  company_short_name=company_short_name,
50
- form_data={"email": email },
60
+ branding=branding_data,
61
+ form_data={"email": email},
51
62
  alert_message=response["error"]), 400
52
63
 
64
+ # Guardamos el mensaje y el icono en la sesión manualmente
65
+ session['alert_message'] = "Si tu correo está registrado, recibirás un enlace para restablecer tu contraseña."
66
+ session['alert_icon'] = "success"
67
+ return redirect(url_for('home', company_short_name=company_short_name))
53
68
 
54
- return render_template('login.html',
55
- company=company,
56
- company_short_name=company_short_name,
57
- alert_icon='success',
58
- alert_message="Hemos enviado un enlace a tu correo para restablecer la contraseña.")
59
69
  except Exception as e:
60
70
  return render_template("error.html",
61
- company=company,
62
71
  company_short_name=company_short_name,
63
- message="Ha ocurrido un error inesperado."), 500
72
+ branding=branding_data,
73
+ message=f"Ha ocurrido un error inesperado: {str(e)}"), 500
64
74
 
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask import request, jsonify
7
+ from flask.views import MethodView
8
+ from iatoolkit.services.history_service import HistoryService
9
+ from iatoolkit.services.auth_service import AuthService
10
+ from injector import inject
11
+ import logging
12
+
13
+
14
+ class HistoryApiView(MethodView):
15
+ """
16
+ Handles requests from the web UI to fetch a user's query history.
17
+ Authentication is based on the active Flask session.
18
+ """
19
+
20
+ @inject
21
+ def __init__(self,
22
+ auth_service: AuthService,
23
+ history_service: HistoryService):
24
+ self.auth_service = auth_service
25
+ self.history_service = history_service
26
+
27
+ def post(self, company_short_name: str):
28
+ # 1. Get the authenticated user's
29
+ auth_result = self.auth_service.verify()
30
+ if not auth_result.get("success"):
31
+ return jsonify(auth_result), auth_result.get("status_code")
32
+
33
+ user_identifier = auth_result.get('user_identifier')
34
+
35
+ try:
36
+ # 2. Call the history service with the unified identifier.
37
+ # The service's signature should now only expect user_identifier.
38
+ response = self.history_service.get_history(
39
+ company_short_name=company_short_name,
40
+ user_identifier=user_identifier
41
+ )
42
+
43
+ if "error" in response:
44
+ # Handle errors reported by the service itself.
45
+ return jsonify({'error_message': response["error"]}), 400
46
+
47
+ return jsonify(response), 200
48
+
49
+ except Exception as e:
50
+ logging.exception(
51
+ f"Unexpected error fetching history for {company_short_name}/{user_identifier}: {e}")
52
+ return jsonify({"error_message": "Ha ocurrido un error inesperado en el servidor."}), 500
@@ -1,34 +1,67 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
1
+ # iatoolkit/views/home_view.py
2
+ import logging
3
+ import os
4
+ from flask import render_template, abort, session, render_template_string
6
5
  from flask.views import MethodView
7
- from flask import render_template, request
8
6
  from injector import inject
9
7
  from iatoolkit.services.profile_service import ProfileService
10
- import os
11
-
8
+ from iatoolkit.services.branding_service import BrandingService
9
+ import logging
12
10
 
13
11
  class HomeView(MethodView):
12
+ """
13
+ Handles the rendering of the company-specific home page with a login widget.
14
+ If the custom template is not found or fails, it renders an error page.
15
+ """
16
+
14
17
  @inject
15
18
  def __init__(self,
16
- profile_service: ProfileService):
19
+ profile_service: ProfileService,
20
+ branding_service: BrandingService):
17
21
  self.profile_service = profile_service
22
+ self.branding_service = branding_service
23
+
24
+ def get(self, company_short_name: str):
25
+ company = self.profile_service.get_company_by_short_name(company_short_name)
26
+
27
+ if not company:
28
+ return render_template('error.html', message="Empresa no encontrada"), 404
29
+
30
+ branding_data = self.branding_service.get_company_branding(company)
31
+ alert_message = session.pop('alert_message', None)
32
+ alert_icon = session.pop('alert_icon', 'error')
33
+
34
+
35
+ # 1. Construimos la ruta al archivo de plantilla específico de la empresa.
36
+ company_template_path = os.path.join(os.getcwd(), f'companies/{company_short_name}/templates/home.html')
37
+
38
+ # 2. Verificamos si el archivo de plantilla personalizado no existe.
39
+ if not os.path.exists(company_template_path):
40
+ return render_template(
41
+ "error.html",
42
+ company_short_name=company_short_name,
43
+ branding=branding_data,
44
+ message=f"La plantilla de la página de inicio para la empresa '{company_short_name}' no está configurada."
45
+ ), 500
46
+
47
+ # 3. Si el archivo existe, intentamos leerlo y renderizarlo.
48
+ try:
49
+ with open(company_template_path, 'r') as f:
50
+ template_string = f.read()
18
51
 
19
- def get(self):
20
- user_agent = request.user_agent
21
- is_mobile = user_agent.platform in ["android", "iphone", "ipad"] or "mobile" in user_agent.string.lower()
22
- alert_message = request.args.get('alert_message', None)
23
- companies = self.profile_service.get_companies()
24
-
25
- # Esta API_KEY para el login
26
- api_key_for_login = os.getenv("IATOOLKIT_API_KEY", "tu_api_key_por_defecto_o_error")
27
-
28
- return render_template('home.html',
29
- companies=companies,
30
- is_mobile=is_mobile,
31
- alert_message=alert_message,
32
- alert_icon='success' if alert_message else None,
33
- api_key=api_key_for_login
34
- )
52
+ # Usamos render_template_string, que entiende el contexto de Flask.
53
+ return render_template_string(
54
+ template_string,
55
+ company=company,
56
+ company_short_name=company_short_name,
57
+ branding=branding_data,
58
+ alert_message=alert_message,
59
+ alert_icon=alert_icon
60
+ )
61
+ except Exception as e:
62
+ return render_template(
63
+ "error.html",
64
+ company_short_name=company_short_name,
65
+ branding=branding_data,
66
+ message=f"Ocurrió un error al procesar la plantilla personalizada de la página de inicio: {str(e)}"
67
+ ), 500
@@ -0,0 +1,14 @@
1
+ # iatoolkit/views/index_view.py
2
+
3
+ from flask import render_template, session
4
+ from flask.views import MethodView
5
+
6
+
7
+ class IndexView(MethodView):
8
+ """
9
+ Handles the rendering of the generic landing page, which no longer depends
10
+ on a specific company.
11
+ """
12
+
13
+ def get(self):
14
+ return render_template('index.html')
@@ -0,0 +1,68 @@
1
+ from flask.views import MethodView
2
+ from injector import inject
3
+ from iatoolkit.services.query_service import QueryService
4
+ from iatoolkit.services.profile_service import ProfileService
5
+ from iatoolkit.services.auth_service import AuthService
6
+ from flask import jsonify, request
7
+ import logging
8
+
9
+
10
+ class InitContextApiView(MethodView):
11
+ """
12
+ API endpoint to force a full context rebuild for a user.
13
+ Handles both web users (via session) and API users (via API Key).
14
+ """
15
+
16
+ @inject
17
+ def __init__(self,
18
+ auth_service: AuthService,
19
+ query_service: QueryService,
20
+ profile_service: ProfileService):
21
+ self.auth_service = auth_service
22
+ self.query_service = query_service
23
+ self.profile_service = profile_service
24
+
25
+ def post(self, company_short_name: str):
26
+ """
27
+ Cleans and rebuilds the context. The user is identified either by
28
+ an active web session or by the external_user_id in the JSON payload
29
+ for API calls.
30
+ """
31
+ # 1. Authenticate the request. This handles both session and API Key.
32
+ auth_result = self.auth_service.verify()
33
+ if not auth_result.get("success"):
34
+ return jsonify(auth_result), auth_result.get("status_code")
35
+
36
+ user_identifier = auth_result.get('user_identifier')
37
+
38
+ try:
39
+ # 2. Execute the forced rebuild sequence using the unified identifier.
40
+ self.query_service.session_context.clear_all_context(company_short_name, user_identifier)
41
+ logging.info(f"Context for {company_short_name}/{user_identifier} has been cleared.")
42
+
43
+ # LLM context is clean, now we can load it again
44
+ self.query_service.prepare_context(
45
+ company_short_name=company_short_name,
46
+ user_identifier=user_identifier
47
+ )
48
+
49
+ self.query_service.finalize_context_rebuild(
50
+ company_short_name=company_short_name,
51
+ user_identifier=user_identifier
52
+ )
53
+
54
+ # 3. Respond with JSON, as this is an API endpoint.
55
+ return jsonify({'status': 'OK', 'message': 'El contexto se ha recargado con éxito.'}), 200
56
+
57
+ except Exception as e:
58
+ logging.exception(f"Error durante la recarga de contexto {user_identifier}: {e}")
59
+ return jsonify({"error_message": str(e)}), 500
60
+
61
+ def options(self, company_short_name):
62
+ """
63
+ Maneja las solicitudes preflight de CORS.
64
+ Su única función es existir y devolver una respuesta exitosa para que
65
+ el middleware Flask-CORS pueda interceptarla y añadir las cabeceras
66
+ 'Access-Control-Allow-*'.
67
+ """
68
+ return {}, 200
@@ -0,0 +1,45 @@
1
+ from flask.views import MethodView
2
+ from flask import request, jsonify
3
+ from injector import inject
4
+ from iatoolkit.services.query_service import QueryService
5
+ from iatoolkit.services.auth_service import AuthService
6
+ from iatoolkit.services.profile_service import ProfileService
7
+ import logging
8
+
9
+ class LLMQueryApiView(MethodView):
10
+ """
11
+ API-only endpoint for submitting queries. Authenticates via API Key.
12
+ """
13
+
14
+ @inject
15
+ def __init__(self, auth_service: AuthService, query_service: QueryService, profile_service: ProfileService):
16
+ self.auth_service = auth_service
17
+ self.query_service = query_service
18
+ self.profile_service = profile_service
19
+
20
+ def post(self, company_short_name: str):
21
+ # 1. Authenticate the API request.
22
+ auth_result = self.auth_service.verify()
23
+ if not auth_result.get("success"):
24
+ return jsonify(auth_result), auth_result.get("status_code")
25
+
26
+ # 2. Get the user identifier from the payload.
27
+ user_identifier = auth_result.get('user_identifier')
28
+
29
+ data = request.get_json()
30
+ if not data:
31
+ return jsonify({"error": "Invalid JSON body"}), 400
32
+
33
+ # 4. Call the unified query service method.
34
+ result = self.query_service.llm_query(
35
+ company_short_name=company_short_name,
36
+ user_identifier=user_identifier,
37
+ question=data.get('question', ''),
38
+ prompt_name=data.get('prompt_name'),
39
+ client_data=data.get('client_data', {}),
40
+ files=data.get('files', [])
41
+ )
42
+ if 'error' in result:
43
+ return jsonify(result), 400
44
+
45
+ return jsonify(result), 200
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import requests
7
+ import json
8
+ import os
9
+ from flask.views import MethodView
10
+ from flask import render_template, request, Response
11
+ from injector import inject
12
+ from iatoolkit.services.profile_service import ProfileService
13
+
14
+
15
+ class LoginSimulationView(MethodView):
16
+ @inject
17
+ def __init__(self,
18
+ profile_service: ProfileService):
19
+ self.profile_service = profile_service
20
+
21
+ def get(self, company_short_name: str = None):
22
+ """Muestra el formulario para iniciar la simulación."""
23
+ return render_template('login_simulation.html',
24
+ company_short_name=company_short_name
25
+ )
26
+
27
+ def post(self, company_short_name: str):
28
+ """
29
+ Recibe el POST del formulario y actúa como un proxy servidor-a-servidor.
30
+ Llama al endpoint 'external_login' y devuelve su respuesta (HTML y headers).
31
+ """
32
+ api_key = os.getenv("IATOOLKIT_API_KEY")
33
+ # Obtenemos la URL base de la petición actual para construir la URL interna
34
+ base_url = request.host_url.rstrip('/')
35
+
36
+ # 1. Obtener el user_identifier del formulario
37
+ user_identifier = request.form.get('external_user_id')
38
+
39
+ if not user_identifier:
40
+ return Response("Error: El campo 'external_user_id' es requerido.", status=400)
41
+
42
+ # 2. Preparar la llamada a la API real de external_login
43
+ target_url = f"{base_url}/{company_short_name}/external_login"
44
+ headers = {
45
+ 'Content-Type': 'application/json',
46
+ 'Authorization': f'Bearer {api_key}'
47
+ }
48
+ # El payload debe ser un diccionario que se convertirá a JSON
49
+ payload = {'user_identifier': user_identifier}
50
+
51
+ try:
52
+ # 3. Llamada POST segura desde este servidor al endpoint de IAToolkit
53
+ internal_response = requests.post(
54
+ target_url,
55
+ headers=headers,
56
+ data=json.dumps(payload),
57
+ timeout=120,
58
+ stream=True # Usamos stream para manejar la respuesta eficientemente
59
+ )
60
+ internal_response.raise_for_status()
61
+
62
+ # 4. Creamos una nueva Response de Flask para el navegador del usuario.
63
+ user_response = Response(
64
+ internal_response.iter_content(chunk_size=1024),
65
+ status=internal_response.status_code
66
+ )
67
+
68
+ # 5. Copiamos TODAS las cabeceras de la respuesta interna a la respuesta final.
69
+ # Esto es CRUCIAL para que las cookies ('Set-Cookie') lleguen al navegador.
70
+ for key, value in internal_response.headers.items():
71
+ # Excluimos cabeceras que no debemos pasar (controladas por el servidor WSGI)
72
+ if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding', 'connection']:
73
+ user_response.headers[key] = value
74
+
75
+ return user_response
76
+
77
+ except requests.exceptions.HTTPError as e:
78
+ error_text = f"Error en la llamada interna a la API: {e.response.status_code}. Respuesta: {e.response.text}"
79
+ return Response(error_text, status=e.response.status_code, mimetype='text/plain')
80
+ except requests.exceptions.RequestException as e:
81
+ return Response(f'Error de conexión con el servicio de IA: {str(e)}', status=502, mimetype='text/plain')