iatoolkit 0.11.0__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 (106) hide show
  1. iatoolkit/base_company.py +11 -3
  2. iatoolkit/common/routes.py +92 -52
  3. iatoolkit/common/session_manager.py +0 -1
  4. iatoolkit/common/util.py +17 -27
  5. iatoolkit/iatoolkit.py +91 -47
  6. iatoolkit/infra/llm_client.py +7 -8
  7. iatoolkit/infra/openai_adapter.py +1 -1
  8. iatoolkit/infra/redis_session_manager.py +48 -2
  9. iatoolkit/locales/en.yaml +144 -0
  10. iatoolkit/locales/es.yaml +140 -0
  11. iatoolkit/repositories/database_manager.py +17 -2
  12. iatoolkit/repositories/models.py +31 -4
  13. iatoolkit/repositories/profile_repo.py +7 -2
  14. iatoolkit/services/auth_service.py +193 -0
  15. iatoolkit/services/branding_service.py +59 -18
  16. iatoolkit/services/dispatcher_service.py +10 -40
  17. iatoolkit/services/excel_service.py +15 -15
  18. iatoolkit/services/help_content_service.py +30 -0
  19. iatoolkit/services/history_service.py +2 -11
  20. iatoolkit/services/i18n_service.py +104 -0
  21. iatoolkit/services/jwt_service.py +15 -24
  22. iatoolkit/services/language_service.py +77 -0
  23. iatoolkit/services/onboarding_service.py +43 -0
  24. iatoolkit/services/profile_service.py +148 -75
  25. iatoolkit/services/query_service.py +124 -81
  26. iatoolkit/services/tasks_service.py +1 -1
  27. iatoolkit/services/user_feedback_service.py +68 -32
  28. iatoolkit/services/user_session_context_service.py +112 -54
  29. iatoolkit/static/images/fernando.jpeg +0 -0
  30. iatoolkit/static/js/chat_feedback_button.js +80 -0
  31. iatoolkit/static/js/chat_help_content.js +124 -0
  32. iatoolkit/static/js/chat_history_button.js +112 -0
  33. iatoolkit/static/js/chat_logout_button.js +36 -0
  34. iatoolkit/static/js/chat_main.js +148 -220
  35. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  36. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  37. iatoolkit/static/js/chat_reload_button.js +35 -0
  38. iatoolkit/static/styles/chat_iatoolkit.css +367 -199
  39. iatoolkit/static/styles/chat_modal.css +98 -76
  40. iatoolkit/static/styles/chat_public.css +107 -0
  41. iatoolkit/static/styles/landing_page.css +182 -0
  42. iatoolkit/static/styles/onboarding.css +169 -0
  43. iatoolkit/system_prompts/query_main.prompt +3 -12
  44. iatoolkit/templates/_company_header.html +20 -0
  45. iatoolkit/templates/_login_widget.html +42 -0
  46. iatoolkit/templates/base.html +40 -20
  47. iatoolkit/templates/change_password.html +57 -36
  48. iatoolkit/templates/chat.html +169 -83
  49. iatoolkit/templates/chat_modals.html +134 -68
  50. iatoolkit/templates/error.html +44 -8
  51. iatoolkit/templates/forgot_password.html +40 -23
  52. iatoolkit/templates/index.html +145 -0
  53. iatoolkit/templates/login_simulation.html +34 -0
  54. iatoolkit/templates/onboarding_shell.html +104 -0
  55. iatoolkit/templates/signup.html +63 -65
  56. iatoolkit/views/base_login_view.py +92 -0
  57. iatoolkit/views/change_password_view.py +56 -30
  58. iatoolkit/views/external_login_view.py +61 -28
  59. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  60. iatoolkit/views/forgot_password_view.py +27 -19
  61. iatoolkit/views/help_content_api_view.py +54 -0
  62. iatoolkit/views/history_api_view.py +56 -0
  63. iatoolkit/views/home_view.py +50 -23
  64. iatoolkit/views/index_view.py +14 -0
  65. iatoolkit/views/init_context_api_view.py +73 -0
  66. iatoolkit/views/llmquery_api_view.py +57 -0
  67. iatoolkit/views/login_simulation_view.py +81 -0
  68. iatoolkit/views/login_view.py +130 -37
  69. iatoolkit/views/logout_api_view.py +49 -0
  70. iatoolkit/views/profile_api_view.py +46 -0
  71. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
  72. iatoolkit/views/signup_view.py +42 -35
  73. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  74. iatoolkit/views/tasks_review_api_view.py +55 -0
  75. iatoolkit/views/user_feedback_api_view.py +60 -0
  76. iatoolkit/views/verify_user_view.py +35 -28
  77. {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
  78. iatoolkit-0.66.2.dist-info/RECORD +119 -0
  79. iatoolkit/common/auth.py +0 -200
  80. iatoolkit/static/images/arrow_up.png +0 -0
  81. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  82. iatoolkit/static/images/logo_clinica.png +0 -0
  83. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  84. iatoolkit/static/images/logo_maxxa.png +0 -0
  85. iatoolkit/static/images/logo_notaria.png +0 -0
  86. iatoolkit/static/images/logo_tarjeta.png +0 -0
  87. iatoolkit/static/images/logo_umayor.png +0 -0
  88. iatoolkit/static/images/upload.png +0 -0
  89. iatoolkit/static/js/chat_feedback.js +0 -115
  90. iatoolkit/static/js/chat_history.js +0 -117
  91. iatoolkit/static/styles/chat_info.css +0 -53
  92. iatoolkit/templates/header.html +0 -31
  93. iatoolkit/templates/home.html +0 -199
  94. iatoolkit/templates/login.html +0 -43
  95. iatoolkit/templates/test.html +0 -9
  96. iatoolkit/views/chat_token_request_view.py +0 -98
  97. iatoolkit/views/chat_view.py +0 -58
  98. iatoolkit/views/download_file_view.py +0 -58
  99. iatoolkit/views/external_chat_login_view.py +0 -95
  100. iatoolkit/views/history_view.py +0 -57
  101. iatoolkit/views/llmquery_view.py +0 -65
  102. iatoolkit/views/tasks_review_view.py +0 -83
  103. iatoolkit/views/user_feedback_view.py +0 -74
  104. iatoolkit-0.11.0.dist-info/RECORD +0 -110
  105. {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
  106. {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -0
iatoolkit/base_company.py CHANGED
@@ -26,10 +26,18 @@ class BaseCompany(ABC):
26
26
  self.company = self.profile_repo.get_company_by_short_name(short_name)
27
27
  return self.company
28
28
 
29
- def _create_company(self, short_name: str, name: str, branding: dict | None = None) -> Company:
29
+ def _create_company(self,
30
+ short_name: str,
31
+ name: str,
32
+ parameters: dict | None = None,
33
+ branding: dict | None = None,
34
+ onboarding_cards: dict | None = None,
35
+ ) -> Company:
30
36
  company_obj = Company(short_name=short_name,
31
37
  name=name,
32
- branding=branding)
38
+ parameters=parameters,
39
+ branding=branding,
40
+ onboarding_cards=onboarding_cards)
33
41
  self.company = self.profile_repo.create_company(company_obj)
34
42
  return self.company
35
43
 
@@ -82,7 +90,7 @@ class BaseCompany(ABC):
82
90
 
83
91
  @abstractmethod
84
92
  # get context specific for this company
85
- def get_user_info(self, **kwargs) -> str:
93
+ def get_user_info(self, user_identifier: str) -> dict:
86
94
  raise NotImplementedError("La subclase debe implementar el método get_user_info()")
87
95
 
88
96
  @abstractmethod
@@ -3,80 +3,102 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from flask import render_template, redirect, flash, url_for,send_from_directory, current_app, abort
7
- from iatoolkit.common.session_manager import SessionManager
6
+ from flask import render_template, redirect, url_for,send_from_directory, current_app, abort
8
7
  from flask import jsonify
9
- from iatoolkit.views.history_view import HistoryView
10
- import os
11
-
12
-
13
- def logout(company_short_name: str):
14
- SessionManager.clear()
15
- flash("Has cerrado sesión correctamente", "info")
16
- if company_short_name:
17
- return redirect(url_for('login', company_short_name=company_short_name))
18
- else:
19
- return redirect(url_for('home'))
20
8
 
21
9
 
22
10
  # this function register all the views
23
11
  def register_views(injector, app):
24
12
 
25
- from iatoolkit.views.llmquery_view import LLMQueryView
26
- from iatoolkit.views.tasks_view import TaskView
27
- from iatoolkit.views.tasks_review_view import TaskReviewView
28
- from iatoolkit.views.home_view import HomeView
29
- from iatoolkit.views.chat_view import ChatView
30
- from iatoolkit.views.login_view import LoginView
31
- from iatoolkit.views.external_chat_login_view import ExternalChatLoginView
13
+ from iatoolkit.views.index_view import IndexView
14
+ from iatoolkit.views.init_context_api_view import InitContextApiView
15
+ from iatoolkit.views.llmquery_api_view import LLMQueryApiView
16
+ from iatoolkit.views.tasks_api_view import TaskApiView
17
+ from iatoolkit.views.tasks_review_api_view import TaskReviewApiView
18
+ from iatoolkit.views.login_simulation_view import LoginSimulationView
32
19
  from iatoolkit.views.signup_view import SignupView
33
20
  from iatoolkit.views.verify_user_view import VerifyAccountView
34
21
  from iatoolkit.views.forgot_password_view import ForgotPasswordView
35
22
  from iatoolkit.views.change_password_view import ChangePasswordView
36
- from iatoolkit.views.file_store_view import FileStoreView
37
- from iatoolkit.views.user_feedback_view import UserFeedbackView
38
- from iatoolkit.views.prompt_view import PromptView
39
- from iatoolkit.views.chat_token_request_view import ChatTokenRequestView
40
- from iatoolkit.views.external_login_view import ExternalLoginView
41
- from iatoolkit.views.download_file_view import DownloadFileView
42
-
43
- app.add_url_rule('/', view_func=HomeView.as_view('home'))
23
+ from iatoolkit.views.file_store_api_view import FileStoreApiView
24
+ from iatoolkit.views.user_feedback_api_view import UserFeedbackApiView
25
+ from iatoolkit.views.prompt_api_view import PromptApiView
26
+ from iatoolkit.views.history_api_view import HistoryApiView
27
+ from iatoolkit.views.help_content_api_view import HelpContentApiView
28
+ from iatoolkit.views.profile_api_view import UserLanguageApiView # <-- Importa la nueva vista
29
+
30
+ from iatoolkit.views.login_view import LoginView, FinalizeContextView
31
+ from iatoolkit.views.external_login_view import ExternalLoginView, RedeemTokenApiView
32
+ from iatoolkit.views.logout_api_view import LogoutApiView
33
+ from iatoolkit.views.home_view import HomeView
44
34
 
45
- # main chat for iatoolkit front
46
- app.add_url_rule('/<company_short_name>/chat', view_func=ChatView.as_view('chat'))
35
+ # iatoolkit home page
36
+ app.add_url_rule('/', view_func=IndexView.as_view('index'))
47
37
 
48
- # front if the company internal portal
49
- app.add_url_rule('/<company_short_name>/chat_login', view_func=ExternalChatLoginView.as_view('external_chat_login'))
50
- app.add_url_rule('/<company_short_name>/external_login/<external_user_id>', view_func=ExternalLoginView.as_view('external_login'))
51
- app.add_url_rule('/auth/chat_token', view_func=ChatTokenRequestView.as_view('chat-token'))
38
+ # company home view
39
+ app.add_url_rule('/<company_short_name>/home', view_func=HomeView.as_view('home'))
52
40
 
53
- # main pages for the iatoolkit frontend
41
+ # login for the iatoolkit integrated frontend
54
42
  app.add_url_rule('/<company_short_name>/login', view_func=LoginView.as_view('login'))
43
+
44
+ # this is the login for external users
45
+ app.add_url_rule('/<company_short_name>/external_login',
46
+ view_func=ExternalLoginView.as_view('external_login'))
47
+
48
+ # this endpoint is called when onboarding_shell finish the context load
49
+ app.add_url_rule(
50
+ '/<company_short_name>/finalize',
51
+ view_func=FinalizeContextView.as_view('finalize_no_token')
52
+ )
53
+
54
+ app.add_url_rule(
55
+ '/<company_short_name>/finalize/<token>',
56
+ view_func=FinalizeContextView.as_view('finalize_with_token')
57
+ )
58
+
59
+ app.add_url_rule(
60
+ '/api/profile/language',
61
+ view_func=UserLanguageApiView.as_view('user_language_api')
62
+ )
63
+
64
+ # logout
65
+ app.add_url_rule('/<company_short_name>/api/logout',
66
+ view_func=LogoutApiView.as_view('logout'))
67
+
68
+ # this endpoint is called by the JS for changing the token for a session
69
+ app.add_url_rule('/<string:company_short_name>/api/redeem_token',
70
+ view_func = RedeemTokenApiView.as_view('redeem_token'))
71
+
72
+ # init (reset) the company context
73
+ app.add_url_rule('/<company_short_name>/api/init-context',
74
+ view_func=InitContextApiView.as_view('init-context'),
75
+ methods=['POST', 'OPTIONS'])
76
+
77
+ # register new user, account verification and forgot password
55
78
  app.add_url_rule('/<company_short_name>/signup',view_func=SignupView.as_view('signup'))
56
- app.add_url_rule('/<company_short_name>/logout', 'logout', logout)
57
- app.add_url_rule('/logout', 'logout', logout)
58
79
  app.add_url_rule('/<company_short_name>/verify/<token>', view_func=VerifyAccountView.as_view('verify_account'))
59
80
  app.add_url_rule('/<company_short_name>/forgot-password', view_func=ForgotPasswordView.as_view('forgot_password'))
60
81
  app.add_url_rule('/<company_short_name>/change-password/<token>', view_func=ChangePasswordView.as_view('change_password'))
61
82
 
62
- # this are backend endpoints mainly
63
- app.add_url_rule('/<company_short_name>/llm_query', view_func=LLMQueryView.as_view('llm_query'))
64
- app.add_url_rule('/<company_short_name>/feedback', view_func=UserFeedbackView.as_view('feedback'))
65
- app.add_url_rule('/<company_short_name>/prompts', view_func=PromptView.as_view('prompt'))
66
- app.add_url_rule('/<company_short_name>/history', view_func=HistoryView.as_view('history'))
67
- app.add_url_rule('/tasks', view_func=TaskView.as_view('tasks'))
68
- app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewView.as_view('tasks-review'))
69
- app.add_url_rule('/load', view_func=FileStoreView.as_view('load'))
83
+ # main chat query, used by the JS in the browser (with credentials)
84
+ # can be used also for executing iatoolkit prompts
85
+ app.add_url_rule('/<company_short_name>/api/llm_query', view_func=LLMQueryApiView.as_view('llm_query_api'))
70
86
 
71
- app.add_url_rule(
72
- '/about', # URL de la ruta
73
- view_func=lambda: render_template('about.html'))
87
+ # open the promt directory
88
+ app.add_url_rule('/<company_short_name>/api/prompts', view_func=PromptApiView.as_view('prompt'))
74
89
 
75
- app.add_url_rule('/version', 'version',
76
- lambda: jsonify({"version": app.config['VERSION']}))
90
+ # toolbar buttons
91
+ app.add_url_rule('/<company_short_name>/api/feedback', view_func=UserFeedbackApiView.as_view('feedback'))
92
+ app.add_url_rule('/<company_short_name>/api/history', view_func=HistoryApiView.as_view('history'))
93
+ app.add_url_rule('/<company_short_name>/api/help-content', view_func=HelpContentApiView.as_view('help-content'))
94
+
95
+ # tasks management endpoints: create task, and review answer
96
+ app.add_url_rule('/tasks', view_func=TaskApiView.as_view('tasks'))
97
+ app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewApiView.as_view('tasks-review'))
98
+
99
+ # this endpoint is for upload documents into the vector store (api-key)
100
+ app.add_url_rule('/api/load', view_func=FileStoreApiView.as_view('load_api'))
77
101
 
78
- app.add_url_rule('/<company_short_name>/<external_user_id>/download-file/<path:filename>',
79
- view_func=DownloadFileView.as_view('download-file'))
80
102
 
81
103
  @app.route('/download/<path:filename>')
82
104
  def download_file(filename):
@@ -99,3 +121,21 @@ def register_views(injector, app):
99
121
  except FileNotFoundError:
100
122
  abort(404)
101
123
 
124
+ # login testing
125
+ app.add_url_rule('/<company_short_name>/login_test',
126
+ view_func=LoginSimulationView.as_view('login_test'))
127
+
128
+ app.add_url_rule(
129
+ '/about', # URL de la ruta
130
+ view_func=lambda: render_template('about.html'))
131
+
132
+ app.add_url_rule('/version', 'version',
133
+ lambda: jsonify({"iatoolkit_version": current_app.config.get('VERSION', 'N/A')}))
134
+
135
+
136
+ # hacer que la raíz '/' vaya al home de iatoolkit
137
+ @app.route('/')
138
+ def root_redirect():
139
+ return redirect(url_for('index'))
140
+
141
+
@@ -5,7 +5,6 @@
5
5
 
6
6
  from flask import session
7
7
 
8
-
9
8
  class SessionManager:
10
9
  @staticmethod
11
10
  def set(key, value):
iatoolkit/common/util.py CHANGED
@@ -9,7 +9,6 @@ from iatoolkit.common.exceptions import IAToolkitException
9
9
  from injector import inject
10
10
  import os
11
11
  from jinja2 import Environment, FileSystemLoader
12
- from iatoolkit.common.session_manager import SessionManager
13
12
  from datetime import datetime, date
14
13
  from decimal import Decimal
15
14
  import yaml
@@ -22,30 +21,8 @@ class Utility:
22
21
  def __init__(self):
23
22
  self.encryption_key = os.getenv('FERNET_KEY')
24
23
 
25
- @staticmethod
26
- def resolve_user_identifier(external_user_id: str = None, local_user_id: int = 0) -> tuple[str, bool]:
27
- """
28
- Resuelve un identificador único de usuario desde external_user_id o local_user_id.
29
-
30
- Lógica:
31
- - Si external_user_id existe y no está vacío: usar external_user_id
32
- - Si no, y local_user_id > 0: obtener email de la sesión actual y retornarlo como ID
33
- - Si ninguno: retornar string vacío
34
-
35
- """
36
- if external_user_id and external_user_id.strip():
37
- return external_user_id.strip(), False
38
- elif local_user_id and local_user_id > 0:
39
- # get the user information from the session
40
- user_data = SessionManager.get('user')
41
- if user_data:
42
- return user_data.get('email', ''), True
43
-
44
- return "", False
45
-
46
24
  def render_prompt_from_template(self,
47
25
  template_pathname: str,
48
- query: str = None,
49
26
  client_data: dict = {},
50
27
  **kwargs) -> str:
51
28
 
@@ -58,8 +35,6 @@ class Utility:
58
35
  env = Environment(loader=FileSystemLoader(template_dir))
59
36
  template = env.get_template(template_file)
60
37
 
61
- kwargs["query"] = query
62
-
63
38
  # add all the keys in client_data to kwargs
64
39
  kwargs.update(client_data)
65
40
 
@@ -74,7 +49,6 @@ class Utility:
74
49
  def render_prompt_from_string(self,
75
50
  template_string: str,
76
51
  searchpath: str | list[str] = None,
77
- query: str = None,
78
52
  client_data: dict = {},
79
53
  **kwargs) -> str:
80
54
  """
@@ -97,7 +71,6 @@ class Utility:
97
71
  env = Environment(loader=loader)
98
72
  template = env.from_string(template_string)
99
73
 
100
- kwargs["query"] = query
101
74
  kwargs.update(client_data)
102
75
 
103
76
  prompt = template.render(**kwargs)
@@ -108,6 +81,23 @@ class Utility:
108
81
  f'No se pudo renderizar el template desde el string, error: {str(e)}') from e
109
82
 
110
83
 
84
+ def get_company_template(self, company_short_name: str, template_name: str) -> str:
85
+ # 1. get the path to the company specific template
86
+ template_path = os.path.join(os.getcwd(), f'companies/{company_short_name}/templates/{template_name}')
87
+ if not os.path.exists(template_path):
88
+ return None
89
+
90
+ # 2. read the file
91
+ try:
92
+ with open(template_path, 'r') as f:
93
+ template_string = f.read()
94
+
95
+ return template_string
96
+ except Exception as e:
97
+ logging.exception(e)
98
+ return None
99
+
100
+
111
101
  def serialize(self, obj):
112
102
  if isinstance(obj, datetime) or isinstance(obj, date):
113
103
  return obj.isoformat()
iatoolkit/iatoolkit.py CHANGED
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from flask import Flask, url_for
6
+ from flask import Flask, url_for, get_flashed_messages
7
7
  from flask_session import Session
8
8
  from flask_injector import FlaskInjector
9
9
  from flask_bcrypt import Bcrypt
@@ -15,10 +15,11 @@ import logging
15
15
  import os
16
16
  from typing import Optional, Dict, Any
17
17
  from iatoolkit.repositories.database_manager import DatabaseManager
18
-
19
- from injector import Binder, singleton, Injector
18
+ from werkzeug.middleware.proxy_fix import ProxyFix
19
+ from injector import Binder, Injector, singleton
20
20
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
21
21
 
22
+ IATOOLKIT_VERSION = "0.66.2"
22
23
 
23
24
  # global variable for the unique instance of IAToolkit
24
25
  _iatoolkit_instance: Optional['IAToolkit'] = None
@@ -51,7 +52,7 @@ class IAToolkit:
51
52
  self.app = None
52
53
  self.db_manager = None
53
54
  self._injector = None
54
- self.version = "0.0.0+dev"
55
+ self.version = IATOOLKIT_VERSION # default version
55
56
 
56
57
  @classmethod
57
58
  def get_instance(cls) -> 'IAToolkit':
@@ -87,7 +88,7 @@ class IAToolkit:
87
88
  # and other integrations, as views are handled manually.
88
89
  FlaskInjector(app=self.app, injector=self._injector)
89
90
 
90
- # Step 6: initialize dispatcher and registered compaies
91
+ # Step 6: initialize dispatcher and registered companies
91
92
  self._init_dispatcher_and_company_instances()
92
93
 
93
94
  # Step 7: Finalize setup within the application context
@@ -95,16 +96,12 @@ class IAToolkit:
95
96
  self._setup_cors()
96
97
  self._setup_additional_services()
97
98
  self._setup_cli_commands()
99
+ self._setup_request_globals()
98
100
  self._setup_context_processors()
99
101
 
100
102
  # Step 8: define the download_dir for excel's
101
103
  self._setup_download_dir()
102
104
 
103
- try:
104
- self.version = _pkg_version("iatoolkit")
105
- except PackageNotFoundError:
106
- pass
107
-
108
105
  logging.info(f"🎉 IAToolkit v{self.version} inicializado correctamente")
109
106
  self._initialized = True
110
107
  return self.app
@@ -113,9 +110,26 @@ class IAToolkit:
113
110
  # get a value from the config dict or the environment variable
114
111
  return self.config.get(key, os.getenv(key, default))
115
112
 
113
+ def _setup_request_globals(self):
114
+ """
115
+ Configures functions to run before each request to set up
116
+ request-global variables, such as language.
117
+ """
118
+ injector = self._injector
119
+
120
+ @self.app.before_request
121
+ def set_request_language():
122
+ """
123
+ Determines and caches the language for the current request in g.lang.
124
+ """
125
+ from iatoolkit.services.language_service import LanguageService
126
+ language_service = injector.get(LanguageService)
127
+ language_service.get_current_language()
128
+
116
129
  def _setup_logging(self):
117
- log_level_str = self._get_config_value('FLASK_ENV', 'production')
118
- log_level = logging.INFO if log_level_str in ('dev', 'development') else logging.WARNING
130
+ # Lee el nivel de log desde una variable de entorno, con 'INFO' como valor por defecto.
131
+ log_level_name = os.getenv('LOG_LEVEL', 'INFO').upper()
132
+ log_level = getattr(logging, log_level_name, logging.INFO)
119
133
 
120
134
  logging.basicConfig(
121
135
  level=log_level,
@@ -144,11 +158,21 @@ class IAToolkit:
144
158
  is_https = self._get_config_value('USE_HTTPS', 'false').lower() == 'true'
145
159
  is_dev = self._get_config_value('FLASK_ENV') == 'development'
146
160
 
161
+ # get the iatoolkit domain
162
+ parsed_url = urlparse(os.getenv('IATOOLKIT_BASE_URL'))
163
+ domain = parsed_url.netloc
164
+
165
+ try:
166
+ self.version = _pkg_version("iatoolkit")
167
+ except PackageNotFoundError:
168
+ pass
169
+
170
+
147
171
  self.app.config.update({
148
172
  'VERSION': self.version,
149
173
  'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
150
- 'SESSION_COOKIE_SAMESITE': "None" if is_https else "Lax",
151
- 'SESSION_COOKIE_SECURE': is_https,
174
+ 'SESSION_COOKIE_SAMESITE': "None",
175
+ 'SESSION_COOKIE_SECURE': True,
152
176
  'SESSION_PERMANENT': False,
153
177
  'SESSION_USE_SIGNER': True,
154
178
  'JWT_SECRET_KEY': self._get_config_value('JWT_SECRET_KEY', 'iatoolkit-jwt-secret'),
@@ -156,6 +180,12 @@ class IAToolkit:
156
180
  'JWT_EXPIRATION_SECONDS_CHAT': int(self._get_config_value('JWT_EXPIRATION_SECONDS_CHAT', 3600))
157
181
  })
158
182
 
183
+ if parsed_url.scheme == 'https':
184
+ self.app.config['PREFERRED_URL_SCHEME'] = 'https'
185
+
186
+ # 2. ProxyFix para no tener problemas con iframes y rutas
187
+ self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_proto=1)
188
+
159
189
  # Configuración para tokenizers en desarrollo
160
190
  if is_dev:
161
191
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
@@ -172,6 +202,15 @@ class IAToolkit:
172
202
  self.db_manager.create_all()
173
203
  logging.info("✅ Base de datos configurada correctamente")
174
204
 
205
+ @self.app.teardown_appcontext
206
+ def remove_session(exception=None):
207
+ """
208
+ Flask calls this after each request.
209
+ It ensures the SQLAlchemy session is properly closed
210
+ and the DB connection is returned to the pool.
211
+ """
212
+ self.db_manager.scoped_session.remove()
213
+
175
214
  def _setup_redis_sessions(self):
176
215
  redis_url = self._get_config_value('REDIS_URL')
177
216
  if not redis_url:
@@ -202,19 +241,19 @@ class IAToolkit:
202
241
 
203
242
  def _setup_cors(self):
204
243
  """🌐 Configura CORS"""
205
- # Origins por defecto para desarrollo
244
+ from iatoolkit.company_registry import get_company_registry
245
+
246
+ # default CORS origin
206
247
  default_origins = [
207
- "http://localhost:5001",
208
- "http://127.0.0.1:5001",
209
248
  os.getenv('IATOOLKIT_BASE_URL')
210
249
  ]
211
250
 
212
- # Obtener origins adicionales desde configuración/env
251
+ # Iterate through the registered company names
213
252
  extra_origins = []
214
- for i in range(1, 11): # Soporte para CORS_ORIGIN_1 a CORS_ORIGIN_10
215
- origin = self._get_config_value(f'CORS_ORIGIN_{i}')
216
- if origin:
217
- extra_origins.append(origin)
253
+ all_company_instances = get_company_registry().get_all_company_instances()
254
+ for company_name, company_instance in all_company_instances.items():
255
+ cors_origin = company_instance.company.parameters.get('cors_origin', [])
256
+ extra_origins += cors_origin
218
257
 
219
258
  all_origins = default_origins + extra_origins
220
259
 
@@ -229,7 +268,6 @@ class IAToolkit:
229
268
 
230
269
  logging.info(f"✅ CORS configurado para: {all_origins}")
231
270
 
232
-
233
271
  def _configure_core_dependencies(self, binder: Binder):
234
272
  """⚙️ Configures all system dependencies."""
235
273
  try:
@@ -241,7 +279,6 @@ class IAToolkit:
241
279
  self._bind_repositories(binder)
242
280
  self._bind_services(binder)
243
281
  self._bind_infrastructure(binder)
244
- self._bind_views(binder)
245
282
 
246
283
  logging.info("✅ Dependencias configuradas correctamente")
247
284
 
@@ -279,6 +316,8 @@ class IAToolkit:
279
316
  from iatoolkit.services.jwt_service import JWTService
280
317
  from iatoolkit.services.dispatcher_service import Dispatcher
281
318
  from iatoolkit.services.branding_service import BrandingService
319
+ from iatoolkit.services.i18n_service import I18nService
320
+ from iatoolkit.services.language_service import LanguageService
282
321
 
283
322
  binder.bind(QueryService, to=QueryService)
284
323
  binder.bind(TaskService, to=TaskService)
@@ -292,39 +331,24 @@ class IAToolkit:
292
331
  binder.bind(JWTService, to=JWTService)
293
332
  binder.bind(Dispatcher, to=Dispatcher)
294
333
  binder.bind(BrandingService, to=BrandingService)
295
-
334
+ binder.bind(I18nService, to=I18nService)
335
+ binder.bind(LanguageService, to=LanguageService)
296
336
 
297
337
  def _bind_infrastructure(self, binder: Binder):
298
338
  from iatoolkit.infra.llm_client import llmClient
299
339
  from iatoolkit.infra.llm_proxy import LLMProxy
300
340
  from iatoolkit.infra.google_chat_app import GoogleChatApp
301
341
  from iatoolkit.infra.mail_app import MailApp
302
- from iatoolkit.common.auth import IAuthentication
342
+ from iatoolkit.services.auth_service import AuthService
303
343
  from iatoolkit.common.util import Utility
304
344
 
305
-
306
-
307
- binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
308
- binder.bind(llmClient, to=llmClient, scope=singleton)
345
+ binder.bind(LLMProxy, to=LLMProxy)
346
+ binder.bind(llmClient, to=llmClient)
309
347
  binder.bind(GoogleChatApp, to=GoogleChatApp)
310
348
  binder.bind(MailApp, to=MailApp)
311
- binder.bind(IAuthentication, to=IAuthentication)
349
+ binder.bind(AuthService, to=AuthService)
312
350
  binder.bind(Utility, to=Utility)
313
351
 
314
- def _bind_views(self, binder: Binder):
315
- """Vincula las vistas después de que el injector ha sido creado"""
316
- from iatoolkit.views.llmquery_view import LLMQueryView
317
- from iatoolkit.views.home_view import HomeView
318
- from iatoolkit.views.chat_view import ChatView
319
- from iatoolkit.views.change_password_view import ChangePasswordView
320
-
321
- binder.bind(HomeView, to=HomeView)
322
- binder.bind(ChatView, to=ChatView)
323
- binder.bind(ChangePasswordView, to=ChangePasswordView)
324
- binder.bind(LLMQueryView, to=LLMQueryView)
325
-
326
- logging.info("✅ Views configuradas correctamente")
327
-
328
352
  def _setup_additional_services(self):
329
353
  Bcrypt(self.app)
330
354
 
@@ -362,12 +386,32 @@ class IAToolkit:
362
386
  @self.app.context_processor
363
387
  def inject_globals():
364
388
  from iatoolkit.common.session_manager import SessionManager
389
+ from iatoolkit.services.profile_service import ProfileService
390
+ from iatoolkit.services.i18n_service import I18nService
391
+
392
+ # Get services from the injector
393
+ profile_service = self._injector.get(ProfileService)
394
+ i18n_service = self._injector.get(I18nService)
395
+
396
+ # The 't' function wrapper no longer needs to determine the language itself.
397
+ # It will be automatically handled by the refactored I18nService.
398
+ def translate_for_template(key: str, **kwargs):
399
+ return i18n_service.t(key, **kwargs)
400
+
401
+ # Get user profile if a session exists
402
+ user_profile = profile_service.get_current_session_info().get('profile', {})
403
+
365
404
  return {
366
405
  'url_for': url_for,
367
406
  'iatoolkit_version': self.version,
368
407
  'app_name': 'IAToolkit',
369
- 'user': SessionManager.get('user'),
370
- 'user_company': SessionManager.get('company_short_name'),
408
+ 'user_identifier': SessionManager.get('user_identifier'),
409
+ 'company_short_name': SessionManager.get('company_short_name'),
410
+ 'user_is_local': user_profile.get('user_is_local'),
411
+ 'user_email': user_profile.get('user_email'),
412
+ 'iatoolkit_base_url': os.environ.get('IATOOLKIT_BASE_URL', ''),
413
+ 'flashed_messages': get_flashed_messages(with_categories=True),
414
+ 't': translate_for_template
371
415
  }
372
416
 
373
417
  def _get_default_static_folder(self) -> str:
@@ -21,6 +21,7 @@ import tiktoken
21
21
  from typing import Dict, Optional, List
22
22
  from iatoolkit.services.dispatcher_service import Dispatcher
23
23
 
24
+ CONTEXT_ERROR_MESSAGE = 'Tu consulta supera el límite de contexto, utiliza el boton de recarga de contexto.'
24
25
 
25
26
  class llmClient:
26
27
  _llm_clients_cache = {} # class attribute, for the clients cache
@@ -116,7 +117,7 @@ class llmClient:
116
117
 
117
118
  # in case of context error
118
119
  if "context_length_exceeded" in str(e):
119
- error_message = 'Tu consulta supera el limite de contexto, sale e ingresa de nuevo a IAToolkit'
120
+ error_message = CONTEXT_ERROR_MESSAGE
120
121
 
121
122
  raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
122
123
 
@@ -256,25 +257,23 @@ class llmClient:
256
257
 
257
258
  # in case of context error
258
259
  if "context_length_exceeded" in str(e):
259
- error_message = 'Tu consulta supera el limite de contexto, sale e ingresa de nuevo a IAToolkit'
260
+ error_message = CONTEXT_ERROR_MESSAGE
260
261
  elif "string_above_max_length" in str(e):
261
- error_message = 'La respuesta es muy larga, trata de filtrar/restringuir tu consulta'
262
+ error_message = 'La respuesta es muy extensa, trata de filtrar/restringuir tu consulta'
262
263
 
263
264
  raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
264
265
 
265
266
  def set_company_context(self,
266
267
  company: Company,
267
268
  company_base_context: str,
268
- model: str = None) -> str:
269
+ model) -> str:
269
270
 
270
- if model:
271
- self.model = model
272
- logging.info(f"initializing model '{self.model}' with company context: {self.count_tokens(company_base_context)} tokens...")
271
+ logging.info(f"initializing model '{model}' with company context: {self.count_tokens(company_base_context)} tokens...")
273
272
 
274
273
  llm_proxy = self.llm_proxy_factory.create_for_company(company)
275
274
  try:
276
275
  response = llm_proxy.create_response(
277
- model=self.model,
276
+ model=model,
278
277
  input=[{
279
278
  "role": "system",
280
279
  "content": company_base_context
@@ -55,7 +55,7 @@ class OpenAIAdapter:
55
55
 
56
56
  # En caso de error de contexto
57
57
  if "context_length_exceeded" in str(e):
58
- error_message = 'Tu consulta supera el limite de contexto, sale e ingresa de nuevo a IAToolkit'
58
+ error_message = 'Tu consulta supera el limite de contexto. Reinicia el contexto con el boton de la barra superior.'
59
59
 
60
60
  raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
61
61