iatoolkit 0.11.0__py3-none-any.whl → 0.71.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 (122) hide show
  1. iatoolkit/__init__.py +2 -6
  2. iatoolkit/base_company.py +9 -29
  3. iatoolkit/cli_commands.py +1 -1
  4. iatoolkit/common/routes.py +96 -52
  5. iatoolkit/common/session_manager.py +2 -1
  6. iatoolkit/common/util.py +17 -27
  7. iatoolkit/company_registry.py +1 -2
  8. iatoolkit/iatoolkit.py +97 -53
  9. iatoolkit/infra/llm_client.py +15 -20
  10. iatoolkit/infra/llm_proxy.py +38 -10
  11. iatoolkit/infra/openai_adapter.py +1 -1
  12. iatoolkit/infra/redis_session_manager.py +48 -2
  13. iatoolkit/locales/en.yaml +167 -0
  14. iatoolkit/locales/es.yaml +163 -0
  15. iatoolkit/repositories/database_manager.py +23 -3
  16. iatoolkit/repositories/document_repo.py +1 -1
  17. iatoolkit/repositories/models.py +35 -10
  18. iatoolkit/repositories/profile_repo.py +3 -2
  19. iatoolkit/repositories/vs_repo.py +26 -20
  20. iatoolkit/services/auth_service.py +193 -0
  21. iatoolkit/services/branding_service.py +70 -25
  22. iatoolkit/services/company_context_service.py +155 -0
  23. iatoolkit/services/configuration_service.py +133 -0
  24. iatoolkit/services/dispatcher_service.py +80 -105
  25. iatoolkit/services/document_service.py +5 -2
  26. iatoolkit/services/embedding_service.py +146 -0
  27. iatoolkit/services/excel_service.py +30 -26
  28. iatoolkit/services/file_processor_service.py +4 -12
  29. iatoolkit/services/history_service.py +7 -16
  30. iatoolkit/services/i18n_service.py +104 -0
  31. iatoolkit/services/jwt_service.py +18 -29
  32. iatoolkit/services/language_service.py +83 -0
  33. iatoolkit/services/load_documents_service.py +100 -113
  34. iatoolkit/services/mail_service.py +9 -4
  35. iatoolkit/services/profile_service.py +152 -76
  36. iatoolkit/services/prompt_manager_service.py +20 -16
  37. iatoolkit/services/query_service.py +208 -96
  38. iatoolkit/services/search_service.py +11 -4
  39. iatoolkit/services/sql_service.py +57 -25
  40. iatoolkit/services/tasks_service.py +1 -1
  41. iatoolkit/services/user_feedback_service.py +72 -34
  42. iatoolkit/services/user_session_context_service.py +112 -54
  43. iatoolkit/static/images/fernando.jpeg +0 -0
  44. iatoolkit/static/js/chat_feedback_button.js +80 -0
  45. iatoolkit/static/js/chat_help_content.js +124 -0
  46. iatoolkit/static/js/chat_history_button.js +110 -0
  47. iatoolkit/static/js/chat_logout_button.js +36 -0
  48. iatoolkit/static/js/chat_main.js +135 -222
  49. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  50. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  51. iatoolkit/static/js/chat_reload_button.js +35 -0
  52. iatoolkit/static/styles/chat_iatoolkit.css +289 -210
  53. iatoolkit/static/styles/chat_modal.css +63 -77
  54. iatoolkit/static/styles/chat_public.css +107 -0
  55. iatoolkit/static/styles/landing_page.css +182 -0
  56. iatoolkit/static/styles/onboarding.css +176 -0
  57. iatoolkit/system_prompts/query_main.prompt +5 -22
  58. iatoolkit/templates/_company_header.html +20 -0
  59. iatoolkit/templates/_login_widget.html +42 -0
  60. iatoolkit/templates/base.html +40 -20
  61. iatoolkit/templates/change_password.html +57 -36
  62. iatoolkit/templates/chat.html +180 -86
  63. iatoolkit/templates/chat_modals.html +138 -68
  64. iatoolkit/templates/error.html +44 -8
  65. iatoolkit/templates/forgot_password.html +40 -23
  66. iatoolkit/templates/index.html +145 -0
  67. iatoolkit/templates/login_simulation.html +45 -0
  68. iatoolkit/templates/onboarding_shell.html +107 -0
  69. iatoolkit/templates/signup.html +63 -65
  70. iatoolkit/views/base_login_view.py +91 -0
  71. iatoolkit/views/change_password_view.py +56 -31
  72. iatoolkit/views/embedding_api_view.py +65 -0
  73. iatoolkit/views/external_login_view.py +61 -28
  74. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
  75. iatoolkit/views/forgot_password_view.py +27 -21
  76. iatoolkit/views/help_content_api_view.py +54 -0
  77. iatoolkit/views/history_api_view.py +56 -0
  78. iatoolkit/views/home_view.py +50 -23
  79. iatoolkit/views/index_view.py +14 -0
  80. iatoolkit/views/init_context_api_view.py +74 -0
  81. iatoolkit/views/llmquery_api_view.py +58 -0
  82. iatoolkit/views/login_simulation_view.py +93 -0
  83. iatoolkit/views/login_view.py +130 -37
  84. iatoolkit/views/logout_api_view.py +49 -0
  85. iatoolkit/views/profile_api_view.py +46 -0
  86. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
  87. iatoolkit/views/signup_view.py +41 -36
  88. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  89. iatoolkit/views/tasks_review_api_view.py +55 -0
  90. iatoolkit/views/user_feedback_api_view.py +60 -0
  91. iatoolkit/views/verify_user_view.py +34 -29
  92. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
  93. iatoolkit-0.71.2.dist-info/RECORD +122 -0
  94. iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
  95. iatoolkit/common/auth.py +0 -200
  96. iatoolkit/static/images/arrow_up.png +0 -0
  97. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  98. iatoolkit/static/images/logo_clinica.png +0 -0
  99. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  100. iatoolkit/static/images/logo_maxxa.png +0 -0
  101. iatoolkit/static/images/logo_notaria.png +0 -0
  102. iatoolkit/static/images/logo_tarjeta.png +0 -0
  103. iatoolkit/static/images/logo_umayor.png +0 -0
  104. iatoolkit/static/images/upload.png +0 -0
  105. iatoolkit/static/js/chat_feedback.js +0 -115
  106. iatoolkit/static/js/chat_history.js +0 -117
  107. iatoolkit/static/styles/chat_info.css +0 -53
  108. iatoolkit/templates/header.html +0 -31
  109. iatoolkit/templates/home.html +0 -199
  110. iatoolkit/templates/login.html +0 -43
  111. iatoolkit/templates/test.html +0 -9
  112. iatoolkit/views/chat_token_request_view.py +0 -98
  113. iatoolkit/views/chat_view.py +0 -58
  114. iatoolkit/views/download_file_view.py +0 -58
  115. iatoolkit/views/external_chat_login_view.py +0 -95
  116. iatoolkit/views/history_view.py +0 -57
  117. iatoolkit/views/llmquery_view.py +0 -65
  118. iatoolkit/views/tasks_review_view.py +0 -83
  119. iatoolkit/views/user_feedback_view.py +0 -74
  120. iatoolkit-0.11.0.dist-info/RECORD +0 -110
  121. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
  122. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
@@ -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:
@@ -41,7 +48,7 @@ class FileStoreView(MethodView):
41
48
  # get the file content from base64
42
49
  content = base64.b64decode(base64_content)
43
50
 
44
- new_document = self.doc_service.load_file_callback(
51
+ new_document = self.doc_service._file_processing_callback(
45
52
  filename=filename,
46
53
  content=content,
47
54
  company=company,
@@ -4,35 +4,47 @@
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, flash
8
8
  from injector import inject
9
9
  from iatoolkit.services.profile_service import ProfileService
10
+ from iatoolkit.services.branding_service import BrandingService
11
+ from iatoolkit.services.i18n_service import I18nService
10
12
  from itsdangerous import URLSafeTimedSerializer
11
13
  import os
12
14
 
13
15
  class ForgotPasswordView(MethodView):
14
16
  @inject
15
- def __init__(self, profile_service: ProfileService):
17
+ def __init__(self, profile_service: ProfileService,
18
+ branding_service: BrandingService,
19
+ i18n_service: I18nService):
16
20
  self.profile_service = profile_service
21
+ self.branding_service = branding_service
22
+ self.i18n_service = i18n_service
23
+
17
24
  self.serializer = URLSafeTimedSerializer(os.getenv("PASS_RESET_KEY"))
18
25
 
19
26
  def get(self, company_short_name: str):
20
27
  # get company info
21
28
  company = self.profile_service.get_company_by_short_name(company_short_name)
22
29
  if not company:
23
- return render_template('error.html', message="Empresa no encontrada"), 404
30
+ return render_template('error.html',
31
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
24
32
 
33
+ branding_data = self.branding_service.get_company_branding(company_short_name)
25
34
  return render_template('forgot_password.html',
26
- company=company,
27
- company_short_name=company_short_name
35
+ company_short_name=company_short_name,
36
+ branding=branding_data
28
37
  )
29
38
 
30
39
  def post(self, company_short_name: str):
31
- company = self.profile_service.get_company_by_short_name(company_short_name)
32
- if not company:
33
- return render_template('error.html', message="Empresa no encontrada"), 404
34
40
 
35
41
  try:
42
+ company = self.profile_service.get_company_by_short_name(company_short_name)
43
+ if not company:
44
+ return render_template('error.html',
45
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
46
+
47
+ branding_data = self.branding_service.get_company_branding(company_short_name)
36
48
  email = request.form.get('email')
37
49
 
38
50
  # create a safe token and url for it
@@ -43,22 +55,16 @@ class ForgotPasswordView(MethodView):
43
55
 
44
56
  response = self.profile_service.forgot_password(email=email, reset_url=reset_url)
45
57
  if "error" in response:
58
+ flash(response["error"], 'error')
46
59
  return render_template(
47
60
  'forgot_password.html',
48
- company=company,
49
61
  company_short_name=company_short_name,
50
- form_data={"email": email },
51
- alert_message=response["error"]), 400
62
+ branding=branding_data,
63
+ form_data={"email": email}), 400
52
64
 
65
+ flash(self.i18n_service.t('flash_messages.forgot_password_success'), 'success')
66
+ return redirect(url_for('home', company_short_name=company_short_name))
53
67
 
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
68
  except Exception as e:
60
- return render_template("error.html",
61
- company=company,
62
- company_short_name=company_short_name,
63
- message="Ha ocurrido un error inesperado."), 500
64
-
69
+ flash(self.i18n_service.t('errors.general.unexpected_error'), 'error')
70
+ return redirect(url_for('home', company_short_name=company_short_name))
@@ -0,0 +1,54 @@
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.configuration_service import ConfigurationService
9
+ from iatoolkit.services.i18n_service import I18nService
10
+ from iatoolkit.services.auth_service import AuthService
11
+ from injector import inject
12
+ import logging
13
+
14
+
15
+ class HelpContentApiView(MethodView):
16
+ """
17
+ Handles requests from the web UI to fetch a user's query history.
18
+ Authentication is based on the active Flask session.
19
+ """
20
+
21
+ @inject
22
+ def __init__(self,
23
+ auth_service: AuthService,
24
+ config_service: ConfigurationService,
25
+ i18n_service: I18nService):
26
+ self.auth_service = auth_service
27
+ self.config_service = config_service
28
+ self.i18n_service = i18n_service
29
+
30
+ def post(self, company_short_name: str):
31
+ try:
32
+ # 1. Get the authenticated user's
33
+ auth_result = self.auth_service.verify()
34
+ if not auth_result.get("success"):
35
+ return jsonify(auth_result), auth_result.get("status_code")
36
+
37
+ user_identifier = auth_result.get('user_identifier')
38
+
39
+ # 2. Call the config service with the unified identifier.
40
+ response = self.config_service.get_configuration(
41
+ company_short_name=company_short_name,
42
+ content_key='help_content' # specific key for this service
43
+ )
44
+
45
+ if "error" in response:
46
+ # Handle errors reported by the service itself.
47
+ return jsonify({'error_message': response["error"]}), 400
48
+
49
+ return jsonify(response), 200
50
+
51
+ except Exception as e:
52
+ logging.exception(
53
+ f"Unexpected error fetching help_content for {company_short_name}: {e}")
54
+ return jsonify({"error_message": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}), 500
@@ -0,0 +1,56 @@
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 iatoolkit.services.i18n_service import I18nService
11
+ from injector import inject
12
+ import logging
13
+
14
+
15
+ class HistoryApiView(MethodView):
16
+ """
17
+ Handles requests from the web UI to fetch a user's query history.
18
+ Authentication is based on the active Flask session.
19
+ """
20
+
21
+ @inject
22
+ def __init__(self,
23
+ auth_service: AuthService,
24
+ history_service: HistoryService,
25
+ i18n_service: I18nService):
26
+ self.auth_service = auth_service
27
+ self.history_service = history_service
28
+ self.i18n_service = i18n_service
29
+
30
+
31
+ def post(self, company_short_name: str):
32
+ try:
33
+ # 1. Get the authenticated user's
34
+ auth_result = self.auth_service.verify()
35
+ if not auth_result.get("success"):
36
+ return jsonify(auth_result), auth_result.get("status_code")
37
+
38
+ user_identifier = auth_result.get('user_identifier')
39
+
40
+ # 2. Call the history service with the unified identifier.
41
+ # The service's signature should now only expect user_identifier.
42
+ response = self.history_service.get_history(
43
+ company_short_name=company_short_name,
44
+ user_identifier=user_identifier
45
+ )
46
+
47
+ if "error" in response:
48
+ # Handle errors reported by the service itself.
49
+ return jsonify({'error_message': response["error"]}), 400
50
+
51
+ return jsonify(response), 200
52
+
53
+ except Exception as e:
54
+ logging.exception(
55
+ f"Unexpected error: {e}")
56
+ return jsonify({"error_message": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}), 500
@@ -1,34 +1,61 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
1
+ # iatoolkit/views/home_view.py
2
+ from flask import render_template, render_template_string
6
3
  from flask.views import MethodView
7
- from flask import render_template, request
8
4
  from injector import inject
9
5
  from iatoolkit.services.profile_service import ProfileService
10
- import os
11
-
6
+ from iatoolkit.services.branding_service import BrandingService
7
+ from iatoolkit.services.i18n_service import I18nService
8
+ from iatoolkit.common.util import Utility
12
9
 
13
10
  class HomeView(MethodView):
11
+ """
12
+ Handles the rendering of the company-specific home page with a login widget.
13
+ If the custom template is not found or fails, it renders an error page.
14
+ """
15
+
14
16
  @inject
15
17
  def __init__(self,
16
- profile_service: ProfileService):
18
+ profile_service: ProfileService,
19
+ branding_service: BrandingService,
20
+ i18n_service: I18nService,
21
+ utility: Utility):
17
22
  self.profile_service = profile_service
23
+ self.branding_service = branding_service
24
+ self.i18n_service = i18n_service
25
+ self.util = utility
26
+
27
+ def get(self, company_short_name: str):
28
+ branding_data = {}
29
+ try:
30
+ company = self.profile_service.get_company_by_short_name(company_short_name)
31
+ if not company:
32
+ return render_template('error.html',
33
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
18
34
 
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()
35
+ branding_data = self.branding_service.get_company_branding(company_short_name)
36
+ home_template = self.util.get_company_template(company_short_name, "home.html")
24
37
 
25
- # Esta API_KEY para el login
26
- api_key_for_login = os.getenv("IATOOLKIT_API_KEY", "tu_api_key_por_defecto_o_error")
38
+ # 2. Verificamos si el archivo de plantilla personalizado no existe.
39
+ if not home_template:
40
+ message = self.i18n_service.t('errors.templates.home_template_not_found', company_name=company_short_name)
41
+ return render_template(
42
+ "error.html",
43
+ company_short_name=company_short_name,
44
+ branding=branding_data,
45
+ message=message
46
+ ), 500
27
47
 
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
- )
48
+ # 3. Si el archivo existe, intentamos leerlo y renderizarlo.
49
+ return render_template_string(
50
+ home_template,
51
+ company_short_name=company_short_name,
52
+ branding=branding_data,
53
+ )
54
+ except Exception as e:
55
+ message = self.i18n_service.t('errors.templates.processing_error', error=str(e))
56
+ return render_template(
57
+ "error.html",
58
+ company_short_name=company_short_name,
59
+ branding=branding_data,
60
+ message=message
61
+ ), 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,74 @@
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 iatoolkit.services.i18n_service import I18nService
7
+ from flask import jsonify, request
8
+ import logging
9
+
10
+
11
+ class InitContextApiView(MethodView):
12
+ """
13
+ API endpoint to force a full context rebuild for a user.
14
+ Handles both web users (via session) and API users (via API Key).
15
+ """
16
+
17
+ @inject
18
+ def __init__(self,
19
+ auth_service: AuthService,
20
+ query_service: QueryService,
21
+ profile_service: ProfileService,
22
+ i18n_service: I18nService):
23
+ self.auth_service = auth_service
24
+ self.query_service = query_service
25
+ self.profile_service = profile_service
26
+ self.i18n_service = i18n_service
27
+
28
+ def post(self, company_short_name: str):
29
+ """
30
+ Cleans and rebuilds the context. The user is identified either by
31
+ an active web session or by the external_user_id in the JSON payload
32
+ for API calls.
33
+ """
34
+ try:
35
+ # 1. Authenticate the request. This handles both session and API Key.
36
+ auth_result = self.auth_service.verify()
37
+ if not auth_result.get("success"):
38
+ return jsonify(auth_result), auth_result.get("status_code")
39
+
40
+ user_identifier = auth_result.get('user_identifier')
41
+
42
+ # check if model was sent as a parameter
43
+ data = request.get_json(silent=True) or {}
44
+ model = data.get('model', '')
45
+
46
+ # reinit the LLM context
47
+ response = self.query_service.init_context(
48
+ company_short_name=company_short_name,
49
+ user_identifier=user_identifier,
50
+ model=model)
51
+
52
+ # Respond with JSON, as this is an API endpoint.
53
+ success_message = self.i18n_service.t('api_responses.context_reloaded_success')
54
+ response_message = {'status': 'OK', 'message': success_message}
55
+
56
+ # if received a response ID with the context, return it
57
+ if response.get('response_id'):
58
+ response_message['response_id'] = response['response_id']
59
+
60
+ return jsonify(response_message), 200
61
+
62
+ except Exception as e:
63
+ logging.exception(f"errors while reloading context: {e}")
64
+ error_message = self.i18n_service.t('errors.general.unexpected_error', error=str(e))
65
+ return jsonify({"error_message": error_message}), 406
66
+
67
+ def options(self, company_short_name):
68
+ """
69
+ Maneja las solicitudes preflight de CORS.
70
+ Su única función es existir y devolver una respuesta exitosa para que
71
+ el middleware Flask-CORS pueda interceptarla y añadir las cabeceras
72
+ 'Access-Control-Allow-*'.
73
+ """
74
+ return {}, 200
@@ -0,0 +1,58 @@
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
+ from iatoolkit.services.i18n_service import I18nService
8
+ import logging
9
+
10
+ class LLMQueryApiView(MethodView):
11
+ """
12
+ API-only endpoint for submitting queries. Authenticates via API Key.
13
+ """
14
+
15
+ @inject
16
+ def __init__(self,
17
+ auth_service: AuthService,
18
+ query_service: QueryService,
19
+ profile_service: ProfileService,
20
+ i18n_service: I18nService):
21
+ self.auth_service = auth_service
22
+ self.query_service = query_service
23
+ self.profile_service = profile_service
24
+ self.i18n_service = i18n_service
25
+
26
+ def post(self, company_short_name: str):
27
+ try:
28
+ # 1. Authenticate the API request.
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
+ # 2. Get the user identifier from the payload.
34
+ user_identifier = auth_result.get('user_identifier')
35
+
36
+ data = request.get_json()
37
+ if not data:
38
+ return jsonify({"error": "Invalid JSON body"}), 400
39
+
40
+ # 4. Call the unified query service method.
41
+ result = self.query_service.llm_query(
42
+ company_short_name=company_short_name,
43
+ user_identifier=user_identifier,
44
+ question=data.get('question', ''),
45
+ prompt_name=data.get('prompt_name'),
46
+ client_data=data.get('client_data', {}),
47
+ response_id = data.get('response_id'),
48
+ files=data.get('files', [])
49
+ )
50
+ if 'error' in result:
51
+ return jsonify(result), 407
52
+
53
+ return jsonify(result), 200
54
+
55
+ except Exception as e:
56
+ logging.exception(
57
+ f"Unexpected error: {e}")
58
+ return jsonify({"error_message": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}), 500
@@ -0,0 +1,93 @@
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
+ from iatoolkit.services.branding_service import BrandingService
14
+
15
+
16
+ class LoginSimulationView(MethodView):
17
+ @inject
18
+ def __init__(self,
19
+ profile_service: ProfileService,
20
+ branding_service: BrandingService):
21
+ self.profile_service = profile_service
22
+ self.branding_service = branding_service
23
+
24
+
25
+ def get(self, company_short_name: str = None):
26
+ company = self.profile_service.get_company_by_short_name(company_short_name)
27
+ if not company:
28
+ return render_template('error.html',
29
+ company_short_name=company_short_name,
30
+ message="Empresa no encontrada"), 404
31
+
32
+ branding_data = self.branding_service.get_company_branding(company_short_name)
33
+
34
+ return render_template('login_simulation.html',
35
+ branding=branding_data,
36
+ company_short_name=company_short_name
37
+ )
38
+
39
+ def post(self, company_short_name: str):
40
+ """
41
+ Recibe el POST del formulario y actúa como un proxy servidor-a-servidor.
42
+ Llama al endpoint 'external_login' y devuelve su respuesta (HTML y headers).
43
+ """
44
+ api_key = os.getenv("IATOOLKIT_API_KEY")
45
+ # Obtenemos la URL base de la petición actual para construir la URL interna
46
+ base_url = request.host_url.rstrip('/')
47
+
48
+ # 1. Obtener el user_identifier del formulario
49
+ user_identifier = request.form.get('external_user_id')
50
+
51
+ if not user_identifier:
52
+ return Response("Error: El campo 'external_user_id' es requerido.", status=400)
53
+
54
+ # 2. Preparar la llamada a la API real de external_login
55
+ target_url = f"{base_url}/{company_short_name}/external_login"
56
+ headers = {
57
+ 'Content-Type': 'application/json',
58
+ 'Authorization': f'Bearer {api_key}'
59
+ }
60
+ # El payload debe ser un diccionario que se convertirá a JSON
61
+ payload = {'user_identifier': user_identifier}
62
+
63
+ try:
64
+ # 3. Llamada POST segura desde este servidor al endpoint de IAToolkit
65
+ internal_response = requests.post(
66
+ target_url,
67
+ headers=headers,
68
+ data=json.dumps(payload),
69
+ timeout=120,
70
+ stream=True # Usamos stream para manejar la respuesta eficientemente
71
+ )
72
+ internal_response.raise_for_status()
73
+
74
+ # 4. Creamos una nueva Response de Flask para el navegador del usuario.
75
+ user_response = Response(
76
+ internal_response.iter_content(chunk_size=1024),
77
+ status=internal_response.status_code
78
+ )
79
+
80
+ # 5. Copiamos TODAS las cabeceras de la respuesta interna a la respuesta final.
81
+ # Esto es CRUCIAL para que las cookies ('Set-Cookie') lleguen al navegador.
82
+ for key, value in internal_response.headers.items():
83
+ # Excluimos cabeceras que no debemos pasar (controladas por el servidor WSGI)
84
+ if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding', 'connection']:
85
+ user_response.headers[key] = value
86
+
87
+ return user_response
88
+
89
+ except requests.exceptions.HTTPError as e:
90
+ error_text = f"Error en la llamada interna a la API: {e.response.status_code}. Respuesta: {e.response.text}"
91
+ return Response(error_text, status=e.response.status_code, mimetype='text/plain')
92
+ except requests.exceptions.RequestException as e:
93
+ return Response(f'Error de conexión con el servicio de IA: {str(e)}', status=502, mimetype='text/plain')