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,65 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask.views import MethodView
7
+ from flask import request, jsonify
8
+ from iatoolkit.services.load_documents_service import LoadDocumentsService
9
+ from iatoolkit.services.auth_service import AuthService
10
+ from iatoolkit.repositories.profile_repo import ProfileRepo
11
+ from injector import inject
12
+ import base64
13
+
14
+
15
+ class FileStoreApiView(MethodView):
16
+ @inject
17
+ def __init__(self,
18
+ auth_service: AuthService,
19
+ doc_service: LoadDocumentsService,
20
+ profile_repo: ProfileRepo,):
21
+ self.auth_service = auth_service
22
+ self.doc_service = doc_service
23
+ self.profile_repo = profile_repo
24
+
25
+ def post(self):
26
+ try:
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")
31
+
32
+ req_data = request.get_json()
33
+ required_fields = ['company', 'filename', 'content']
34
+ for field in required_fields:
35
+ if field not in req_data:
36
+ return jsonify({"error": f"El campo {field} es requerido"}), 400
37
+
38
+ company_short_name = req_data.get('company', '')
39
+ filename = req_data.get('filename', False)
40
+ base64_content = req_data.get('content', '')
41
+ metadata = req_data.get('metadata', {})
42
+
43
+ # get company
44
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
45
+ if not company:
46
+ return jsonify({"error": f"La empresa {company_short_name} no existe"}), 400
47
+
48
+ # get the file content from base64
49
+ content = base64.b64decode(base64_content)
50
+
51
+ new_document = self.doc_service.load_file_callback(
52
+ filename=filename,
53
+ content=content,
54
+ company=company,
55
+ context={'metadata': metadata})
56
+
57
+ return jsonify({
58
+ "document_id": new_document.id,
59
+ }), 200
60
+
61
+ except Exception as e:
62
+ response = jsonify({"error": str(e)})
63
+ response.status_code = 500
64
+
65
+ return response
@@ -0,0 +1,72 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask.views import MethodView
7
+ from flask import render_template, request, url_for, redirect, session, flash
8
+ from injector import inject
9
+ from iatoolkit.services.profile_service import ProfileService
10
+ from iatoolkit.services.branding_service import BrandingService
11
+ from iatoolkit.services.i18n_service import I18nService
12
+ from itsdangerous import URLSafeTimedSerializer
13
+ import os
14
+
15
+ class ForgotPasswordView(MethodView):
16
+ @inject
17
+ def __init__(self, profile_service: ProfileService,
18
+ branding_service: BrandingService,
19
+ i18n_service: I18nService):
20
+ self.profile_service = profile_service
21
+ self.branding_service = branding_service
22
+ self.i18n_service = i18n_service
23
+
24
+ self.serializer = URLSafeTimedSerializer(os.getenv("PASS_RESET_KEY"))
25
+
26
+ def get(self, company_short_name: str):
27
+ # get company info
28
+ company = self.profile_service.get_company_by_short_name(company_short_name)
29
+ if not company:
30
+ return render_template('error.html',
31
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
32
+
33
+ branding_data = self.branding_service.get_company_branding(company)
34
+ return render_template('forgot_password.html',
35
+ company=company,
36
+ company_short_name=company_short_name,
37
+ branding=branding_data
38
+ )
39
+
40
+ def post(self, company_short_name: str):
41
+
42
+ try:
43
+ company = self.profile_service.get_company_by_short_name(company_short_name)
44
+ if not company:
45
+ return render_template('error.html',
46
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
47
+
48
+ branding_data = self.branding_service.get_company_branding(company)
49
+ email = request.form.get('email')
50
+
51
+ # create a safe token and url for it
52
+ token = self.serializer.dumps(email, salt='password-reset')
53
+ reset_url = url_for('change_password',
54
+ company_short_name=company_short_name,
55
+ token=token, _external=True)
56
+
57
+ response = self.profile_service.forgot_password(email=email, reset_url=reset_url)
58
+ if "error" in response:
59
+ flash(response["error"], 'error')
60
+ return render_template(
61
+ 'forgot_password.html',
62
+ company=company,
63
+ company_short_name=company_short_name,
64
+ branding=branding_data,
65
+ form_data={"email": email}), 400
66
+
67
+ flash(self.i18n_service.t('flash_messages.forgot_password_success'), 'success')
68
+ return redirect(url_for('home', company_short_name=company_short_name))
69
+
70
+ except Exception as e:
71
+ flash(self.i18n_service.t('errors.general.unexpected_error'), 'error')
72
+ 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.help_content_service import HelpContentService
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
+ help_content_service: HelpContentService,
25
+ i18n_service: I18nService):
26
+ self.auth_service = auth_service
27
+ self.help_content_service = help_content_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 history service with the unified identifier.
40
+ # The service's signature should now only expect user_identifier.
41
+ response = self.help_content_service.get_content(
42
+ company_short_name=company_short_name
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}/{user_identifier}: {e}")
54
+ return jsonify({"error_message": self.i18n_service.t('errors.general.unexpected_error')}), 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')}), 500
@@ -0,0 +1,61 @@
1
+ # iatoolkit/views/home_view.py
2
+ from flask import render_template, render_template_string
3
+ from flask.views import MethodView
4
+ from injector import inject
5
+ from iatoolkit.services.profile_service import ProfileService
6
+ from iatoolkit.services.branding_service import BrandingService
7
+ from iatoolkit.services.i18n_service import I18nService
8
+ from iatoolkit.common.util import Utility
9
+
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
+
16
+ @inject
17
+ def __init__(self,
18
+ profile_service: ProfileService,
19
+ branding_service: BrandingService,
20
+ i18n_service: I18nService,
21
+ utility: Utility):
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
+ try:
29
+ company = self.profile_service.get_company_by_short_name(company_short_name)
30
+ if not company:
31
+ return render_template('error.html',
32
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
33
+
34
+ branding_data = self.branding_service.get_company_branding(company)
35
+ home_template = self.util.get_company_template(company_short_name, "home.html")
36
+
37
+ # 2. Verificamos si el archivo de plantilla personalizado no existe.
38
+ if not home_template:
39
+ message = self.i18n_service.t('errors.templates.home_template_not_found', company_name=company_short_name)
40
+ return render_template(
41
+ "error.html",
42
+ company_short_name=company_short_name,
43
+ branding=branding_data,
44
+ message=message
45
+ ), 500
46
+
47
+ # 3. Si el archivo existe, intentamos leerlo y renderizarlo.
48
+ return render_template_string(
49
+ home_template,
50
+ company=company,
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,73 @@
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
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
+ # 2. Execute the forced rebuild sequence using the unified identifier.
43
+ self.query_service.session_context.clear_all_context(company_short_name, user_identifier)
44
+ logging.info(f"Context for {company_short_name}/{user_identifier} has been cleared.")
45
+
46
+ # LLM context is clean, now we can load it again
47
+ self.query_service.prepare_context(
48
+ company_short_name=company_short_name,
49
+ user_identifier=user_identifier
50
+ )
51
+
52
+ self.query_service.finalize_context_rebuild(
53
+ company_short_name=company_short_name,
54
+ user_identifier=user_identifier
55
+ )
56
+
57
+ # 3. Respond with JSON, as this is an API endpoint.
58
+ success_message = self.i18n_service.t('api_responses.context_reloaded_success')
59
+ return jsonify({'status': 'OK', 'message': success_message}), 200
60
+
61
+ except Exception as e:
62
+ logging.exception(f"errors while reloading context: {e}")
63
+ error_message = self.i18n_service.t('errors.general.unexpected_error')
64
+ return jsonify({"error_message": error_message}), 500
65
+
66
+ def options(self, company_short_name):
67
+ """
68
+ Maneja las solicitudes preflight de CORS.
69
+ Su única función es existir y devolver una respuesta exitosa para que
70
+ el middleware Flask-CORS pueda interceptarla y añadir las cabeceras
71
+ 'Access-Control-Allow-*'.
72
+ """
73
+ return {}, 200
@@ -0,0 +1,57 @@
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
+ files=data.get('files', [])
48
+ )
49
+ if 'error' in result:
50
+ return jsonify(result), 400
51
+
52
+ return jsonify(result), 200
53
+
54
+ except Exception as e:
55
+ logging.exception(
56
+ f"Unexpected error: {e}")
57
+ return jsonify({"error_message": self.i18n_service.t('errors.general.unexpected_error')}), 500
@@ -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')