iatoolkit 0.91.1__py3-none-any.whl → 1.4.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 (69) hide show
  1. iatoolkit/__init__.py +6 -4
  2. iatoolkit/base_company.py +0 -16
  3. iatoolkit/cli_commands.py +3 -14
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/interfaces/__init__.py +0 -0
  6. iatoolkit/common/interfaces/asset_storage.py +34 -0
  7. iatoolkit/common/interfaces/database_provider.py +38 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +42 -5
  10. iatoolkit/common/util.py +11 -12
  11. iatoolkit/company_registry.py +5 -0
  12. iatoolkit/core.py +51 -20
  13. iatoolkit/infra/llm_providers/__init__.py +0 -0
  14. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  15. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  16. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  17. iatoolkit/infra/llm_proxy.py +235 -134
  18. iatoolkit/infra/llm_response.py +5 -0
  19. iatoolkit/locales/en.yaml +124 -2
  20. iatoolkit/locales/es.yaml +122 -0
  21. iatoolkit/repositories/database_manager.py +44 -19
  22. iatoolkit/repositories/document_repo.py +7 -0
  23. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  24. iatoolkit/repositories/llm_query_repo.py +2 -0
  25. iatoolkit/repositories/models.py +72 -79
  26. iatoolkit/repositories/profile_repo.py +59 -3
  27. iatoolkit/repositories/vs_repo.py +22 -24
  28. iatoolkit/services/company_context_service.py +88 -39
  29. iatoolkit/services/configuration_service.py +157 -68
  30. iatoolkit/services/dispatcher_service.py +21 -3
  31. iatoolkit/services/file_processor_service.py +0 -5
  32. iatoolkit/services/history_manager_service.py +43 -24
  33. iatoolkit/services/knowledge_base_service.py +412 -0
  34. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
  35. iatoolkit/services/load_documents_service.py +18 -47
  36. iatoolkit/services/profile_service.py +32 -4
  37. iatoolkit/services/prompt_service.py +32 -30
  38. iatoolkit/services/query_service.py +51 -26
  39. iatoolkit/services/sql_service.py +105 -74
  40. iatoolkit/services/tool_service.py +26 -11
  41. iatoolkit/services/user_session_context_service.py +115 -63
  42. iatoolkit/static/js/chat_main.js +44 -4
  43. iatoolkit/static/js/chat_model_selector.js +227 -0
  44. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  45. iatoolkit/static/js/chat_reload_button.js +4 -1
  46. iatoolkit/static/styles/chat_iatoolkit.css +58 -2
  47. iatoolkit/static/styles/llm_output.css +34 -1
  48. iatoolkit/system_prompts/query_main.prompt +26 -2
  49. iatoolkit/templates/base.html +13 -0
  50. iatoolkit/templates/chat.html +44 -2
  51. iatoolkit/templates/onboarding_shell.html +0 -1
  52. iatoolkit/views/base_login_view.py +7 -2
  53. iatoolkit/views/chat_view.py +76 -0
  54. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  55. iatoolkit/views/load_document_api_view.py +14 -10
  56. iatoolkit/views/login_view.py +8 -3
  57. iatoolkit/views/rag_api_view.py +216 -0
  58. iatoolkit/views/users_api_view.py +33 -0
  59. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/METADATA +4 -4
  60. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/RECORD +64 -56
  61. iatoolkit/repositories/tasks_repo.py +0 -52
  62. iatoolkit/services/search_service.py +0 -55
  63. iatoolkit/services/tasks_service.py +0 -188
  64. iatoolkit/views/tasks_api_view.py +0 -72
  65. iatoolkit/views/tasks_review_api_view.py +0 -55
  66. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
  67. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
  68. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  69. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,10 @@
2
2
  # Copyright (c) 2024 Fernando Libedinsky
3
3
  # Product: IAToolkit
4
4
 
5
- from pathlib import Path
6
5
  from iatoolkit.repositories.models import Company
6
+ from iatoolkit.common.interfaces.asset_storage import AssetRepository, AssetType
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
9
- from iatoolkit.common.exceptions import IAToolkitException
10
9
  from iatoolkit.common.util import Utility
11
10
  from injector import inject
12
11
  import logging
@@ -21,9 +20,11 @@ class ConfigurationService:
21
20
 
22
21
  @inject
23
22
  def __init__(self,
23
+ asset_repo: AssetRepository,
24
24
  llm_query_repo: LLMQueryRepo,
25
25
  profile_repo: ProfileRepo,
26
26
  utility: Utility):
27
+ self.asset_repo = asset_repo
27
28
  self.llm_query_repo = llm_query_repo
28
29
  self.profile_repo = profile_repo
29
30
  self.utility = utility
@@ -45,7 +46,30 @@ class ConfigurationService:
45
46
  self._ensure_config_loaded(company_short_name)
46
47
  return self._loaded_configs[company_short_name].get(content_key)
47
48
 
48
- def load_configuration(self, company_short_name: str, company_instance):
49
+ def get_llm_configuration(self, company_short_name: str):
50
+ """
51
+ Convenience helper to obtain the 'llm' configuration block for a company.
52
+ Kept separate from get_configuration() to avoid coupling tests that
53
+ assert the number of calls to get_configuration().
54
+ """
55
+ default_llm_model = None
56
+ available_llm_models = []
57
+ self._ensure_config_loaded(company_short_name)
58
+ llm_config = self._loaded_configs[company_short_name].get("llm")
59
+ if llm_config:
60
+ default_llm_model = llm_config.get("model")
61
+ available_llm_models = llm_config.get('available_models') or []
62
+
63
+ # fallback: if no explicit list of models is provided, use the default model
64
+ if not available_llm_models and default_llm_model:
65
+ available_llm_models = [{
66
+ "id": default_llm_model,
67
+ "label": default_llm_model,
68
+ "description": "Modelo por defecto configurado para esta compañía."
69
+ }]
70
+ return default_llm_model, available_llm_models
71
+
72
+ def load_configuration(self, company_short_name: str):
49
73
  """
50
74
  Main entry point for configuring a company instance.
51
75
  This method is invoked by the dispatcher for each registered company.
@@ -54,71 +78,99 @@ class ConfigurationService:
54
78
 
55
79
  # 1. Load the main configuration file and supplementary content files
56
80
  config = self._load_and_merge_configs(company_short_name)
81
+ if config:
82
+ # 2. create/update company in database
83
+ self._register_company_database(config)
57
84
 
58
- # 2. Register core company details and get the database object
59
- self._register_core_details(company_instance, config)
60
-
61
- # 3. Register databases
62
- self._register_data_sources(company_short_name, config)
85
+ # 3. Register tools
86
+ self._register_tools(company_short_name, config)
63
87
 
64
- # 4. Register tools
65
- self._register_tools(company_instance, config)
88
+ # 4. Register prompt categories and prompts
89
+ self._register_prompts(company_short_name, config)
66
90
 
67
- # 5. Register prompt categories and prompts
68
- self._register_prompts(company_instance, config)
69
-
70
- # 6. Link the persisted Company object back to the running instance
71
- company_instance.company_short_name = company_short_name
72
- company_instance.id = company_instance.company.id
91
+ # 5. Register Knowledge base information
92
+ self._register_knowledge_base(company_short_name, config)
73
93
 
74
94
  # Final step: validate the configuration against platform
75
- self._validate_configuration(company_short_name, config)
95
+ errors = self._validate_configuration(company_short_name, config)
76
96
 
77
97
  logging.info(f"✅ Company '{company_short_name}' configured successfully.")
78
-
98
+ return config, errors
79
99
 
80
100
  def _load_and_merge_configs(self, company_short_name: str) -> dict:
81
101
  """
82
102
  Loads the main company.yaml and merges data from supplementary files
83
- specified in the 'content_files' section.
103
+ specified in the 'content_files' section using AssetRepository.
84
104
  """
85
- config_dir = Path("companies") / company_short_name / "config"
86
- main_config_path = config_dir / "company.yaml"
87
-
88
- if not main_config_path.exists():
89
- raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
90
-
91
- config = self.utility.load_schema_from_yaml(main_config_path)
105
+ main_config_filename = "company.yaml"
106
+
107
+ # verify existence of the main configuration file
108
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, main_config_filename):
109
+ # raise FileNotFoundError(f"Main configuration file not found: {main_config_filename}")
110
+ logging.exception(f"Main configuration file not found: {main_config_filename}")
111
+
112
+ # return the minimal configuration needed for starting the IAToolkit
113
+ # this is a for solving a chicken/egg problem when trying to migrate the configuration
114
+ # from filesystem to database in enterprise installation
115
+ # see create_assets cli command in enterprise-iatoolkit)
116
+ return {
117
+ 'id': company_short_name,
118
+ 'name': company_short_name,
119
+ 'llm': {'model': 'gpt-5', 'provider_api_keys': {'openai':''} },
120
+ }
121
+
122
+ # read text and parse
123
+ yaml_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, main_config_filename)
124
+ config = self.utility.load_yaml_from_string(yaml_content)
125
+ if not config:
126
+ return {}
92
127
 
93
128
  # Load and merge supplementary content files (e.g., onboarding_cards)
94
- for key, file_path in config.get('help_files', {}).items():
95
- supplementary_path = config_dir / file_path
96
- if supplementary_path.exists():
97
- config[key] = self.utility.load_schema_from_yaml(supplementary_path)
129
+ for key, filename in config.get('help_files', {}).items():
130
+ if self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
131
+ supp_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, filename)
132
+ config[key] = self.utility.load_yaml_from_string(supp_content)
98
133
  else:
99
- logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
100
- config[key] = None # Ensure the key exists but is empty
134
+ logging.warning(f"⚠️ Warning: Content file not found: {filename}")
135
+ config[key] = None
101
136
 
102
137
  return config
103
138
 
104
- def _register_core_details(self, company_instance, config: dict) -> Company:
139
+ def _register_company_database(self, config: dict) -> Company:
105
140
  # register the company in the database: create_or_update logic
141
+ if not config:
142
+ return None
106
143
 
107
- company_obj = Company(short_name=config['id'],
108
- name=config['name'],
144
+ # create or update the company in database
145
+ company_obj = Company(short_name=config.get('id'),
146
+ name=config.get('name'),
109
147
  parameters=config.get('parameters', {}))
110
148
  company = self.profile_repo.create_company(company_obj)
111
149
 
112
- # save company object with the instance
113
- company_instance.company = company
150
+ # save company object with the configuration
151
+ config['company'] = company
152
+
114
153
  return company
115
154
 
116
- def _register_data_sources(self, company_short_name: str, config: dict):
155
+ def register_data_sources(self,
156
+ company_short_name: str,
157
+ config: dict = None):
117
158
  """
118
159
  Reads the data_sources config and registers databases with SqlService.
119
160
  Uses Lazy Loading to avoid circular dependency.
161
+
162
+ Public method: Can be called externally after initialization (e.g. by Enterprise)
163
+ to re-register sources once new factories (like 'bridge') are available.
120
164
  """
121
- # Lazy import to avoid circular dependency: ConfigService -> SqlService -> I18n -> ConfigService
165
+
166
+ # If config is not provided, try to load it from cache
167
+ if config is None:
168
+ self._ensure_config_loaded(company_short_name)
169
+ config = self._loaded_configs.get(company_short_name)
170
+
171
+ if not config:
172
+ return
173
+
122
174
  from iatoolkit import current_iatoolkit
123
175
  from iatoolkit.services.sql_service import SqlService
124
176
  sql_service = current_iatoolkit().get_injector().get(SqlService)
@@ -129,25 +181,46 @@ class ConfigurationService:
129
181
  if not sql_sources:
130
182
  return
131
183
 
132
- logging.info(f"🛢️ Registering databases for '{company_short_name}'...")
184
+ logging.info(f"🛢️ Registering databases for '{company_short_name}'...")
133
185
 
134
- for db_config in sql_sources:
135
- db_name = db_config.get('database')
136
- db_schema = db_config.get('schema', 'public')
137
- db_env_var = db_config.get('connection_string_env')
138
-
139
- # resolve the URI
140
- db_uri = os.getenv(db_env_var) if db_env_var else None
186
+ for source in sql_sources:
187
+ db_name = source.get('database')
188
+ if not db_name:
189
+ continue
141
190
 
142
- if not db_uri:
191
+ # Prepare the config dictionary for the factory
192
+ db_config = {
193
+ 'database': db_name,
194
+ 'schema': source.get('schema', 'public'),
195
+ 'connection_type': source.get('connection_type', 'direct'),
196
+
197
+ # Pass through keys needed for Bridge or other plugins
198
+ 'bridge_id': source.get('bridge_id'),
199
+ 'timeout': source.get('timeout')
200
+ }
201
+
202
+ # Resolve URI if env var is present (Required for 'direct', optional for others)
203
+ db_env_var = source.get('connection_string_env')
204
+ if db_env_var:
205
+ db_uri = os.getenv(db_env_var)
206
+ if db_uri:
207
+ db_config['db_uri'] = db_uri
208
+
209
+ # Validation: 'direct' connections MUST have a URI
210
+ if db_config['connection_type'] == 'direct' and not db_config.get('db_uri'):
143
211
  logging.error(
144
212
  f"-> Skipping DB '{db_name}' for '{company_short_name}': missing URI in env '{db_env_var}'.")
145
213
  continue
146
214
 
215
+ elif db_config['connection_type'] == 'bridge' and not db_config.get('bridge_id'):
216
+ logging.error(
217
+ f"-> Skipping DB '{db_name}' for '{company_short_name}': missing bridge_id in configuration.")
218
+ continue
219
+
147
220
  # Register with the SQL service
148
- sql_service.register_database(db_uri, db_name, db_schema)
221
+ sql_service.register_database(company_short_name, db_name, db_config)
149
222
 
150
- def _register_tools(self, company_instance, config: dict):
223
+ def _register_tools(self, company_short_name: str, config: dict):
151
224
  """creates in the database each tool defined in the YAML."""
152
225
  # Lazy import and resolve ToolService locally
153
226
  from iatoolkit import current_iatoolkit
@@ -155,9 +228,9 @@ class ConfigurationService:
155
228
  tool_service = current_iatoolkit().get_injector().get(ToolService)
156
229
 
157
230
  tools_config = config.get('tools', [])
158
- tool_service.sync_company_tools(company_instance, tools_config)
231
+ tool_service.sync_company_tools(company_short_name, tools_config)
159
232
 
160
- def _register_prompts(self, company_instance, config: dict):
233
+ def _register_prompts(self, company_short_name: str, config: dict):
161
234
  """
162
235
  Delegates prompt synchronization to PromptService.
163
236
  """
@@ -170,11 +243,24 @@ class ConfigurationService:
170
243
  categories_config = config.get('prompt_categories', [])
171
244
 
172
245
  prompt_service.sync_company_prompts(
173
- company_instance=company_instance,
246
+ company_short_name=company_short_name,
174
247
  prompts_config=prompts_config,
175
248
  categories_config=categories_config
176
249
  )
177
250
 
251
+ def _register_knowledge_base(self, company_short_name: str, config: dict):
252
+ # Lazy import to avoid circular dependency
253
+ from iatoolkit import current_iatoolkit
254
+ from iatoolkit.services.knowledge_base_service import KnowledgeBaseService
255
+ knowledge_base = current_iatoolkit().get_injector().get(KnowledgeBaseService)
256
+
257
+ kb_config = config.get('knowledge_base', {})
258
+ categories_config = kb_config.get('collections', [])
259
+
260
+ # sync collection types in database
261
+ knowledge_base.sync_collection_types(company_short_name, categories_config)
262
+
263
+
178
264
  def _validate_configuration(self, company_short_name: str, config: dict):
179
265
  """
180
266
  Validates the structure and consistency of the company.yaml configuration.
@@ -182,13 +268,15 @@ class ConfigurationService:
182
268
  Raises IAToolkitException if any validation error is found.
183
269
  """
184
270
  errors = []
185
- config_dir = Path("companies") / company_short_name / "config"
186
- prompts_dir = Path("companies") / company_short_name / "prompts"
187
271
 
188
272
  # Helper to collect errors
189
273
  def add_error(section, message):
190
274
  errors.append(f"[{section}] {message}")
191
275
 
276
+ if not config:
277
+ add_error("General", "Configuration file missing or with errors, check the application logs.")
278
+ return errors
279
+
192
280
  # 1. Top-level keys
193
281
  if not config.get("id"):
194
282
  add_error("General", "Missing required key: 'id'")
@@ -204,8 +292,8 @@ class ConfigurationService:
204
292
  else:
205
293
  if not config.get("llm", {}).get("model"):
206
294
  add_error("llm", "Missing required key: 'model'")
207
- if not config.get("llm", {}).get("api-key"):
208
- add_error("llm", "Missing required key: 'api-key'")
295
+ if not config.get("llm", {}).get("provider_api_keys"):
296
+ add_error("llm", "Missing required key: 'provider_api_keys'")
209
297
 
210
298
  # 3. Embedding Provider
211
299
  if isinstance(config.get("embedding_provider"), dict):
@@ -220,8 +308,12 @@ class ConfigurationService:
220
308
  for i, source in enumerate(config.get("data_sources", {}).get("sql", [])):
221
309
  if not source.get("database"):
222
310
  add_error(f"data_sources.sql[{i}]", "Missing required key: 'database'")
223
- if not source.get("connection_string_env"):
311
+
312
+ connection_type = source.get("connection_type")
313
+ if connection_type == 'direct' and not source.get("connection_string_env"):
224
314
  add_error(f"data_sources.sql[{i}]", "Missing required key: 'connection_string_env'")
315
+ elif connection_type == 'bridge' and not source.get("bridge_id"):
316
+ add_error(f"data_sources.sql[{i}]", "Missing bridge_id'")
225
317
 
226
318
  # 5. Tools
227
319
  for i, tool in enumerate(config.get("tools", [])):
@@ -242,9 +334,9 @@ class ConfigurationService:
242
334
  if not prompt_name:
243
335
  add_error(f"prompts[{i}]", "Missing required key: 'name'")
244
336
  else:
245
- prompt_file = prompts_dir / f"{prompt_name}.prompt"
246
- if not prompt_file.is_file():
247
- add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_file}")
337
+ prompt_filename = f"{prompt_name}.prompt"
338
+ if not self.asset_repo.exists(company_short_name, AssetType.PROMPT, prompt_filename):
339
+ add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_filename}")
248
340
 
249
341
  prompt_description = prompt.get("description")
250
342
  if not prompt_description:
@@ -289,9 +381,9 @@ class ConfigurationService:
289
381
  if not filename:
290
382
  add_error(f"help_files.{key}", "Filename cannot be empty.")
291
383
  continue
292
- help_file_path = config_dir / filename
293
- if not help_file_path.is_file():
294
- add_error(f"help_files.{key}", f"Help file not found: {help_file_path}")
384
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
385
+ add_error(f"help_files.{key}", f"Help file not found: {filename}")
386
+
295
387
 
296
388
  # If any errors were found, log all messages and raise an exception
297
389
  if errors:
@@ -299,8 +391,5 @@ class ConfigurationService:
299
391
  f" - {e}" for e in errors)
300
392
  logging.error(error_summary)
301
393
 
302
- raise IAToolkitException(
303
- IAToolkitException.ErrorType.CONFIG_ERROR,
304
- 'company.yaml validation errors'
305
- )
394
+ return errors
306
395
 
@@ -59,13 +59,25 @@ class Dispatcher:
59
59
  self.setup_iatoolkit_system()
60
60
 
61
61
  # Loads the configuration of every company: company.yaml file
62
- for company_name, company_instance in self.company_instances.items():
62
+ for company_short_name, company_instance in self.company_instances.items():
63
63
  try:
64
64
  # read company configuration from company.yaml
65
- self.config_service.load_configuration(company_name, company_instance)
65
+ config, errors = self.config_service.load_configuration(company_short_name)
66
+
67
+ '''
68
+ if errors:
69
+ raise IAToolkitException(
70
+ IAToolkitException.ErrorType.CONFIG_ERROR,
71
+ 'company.yaml validation errors'
72
+ )
73
+ '''
74
+
75
+ # complement the instance self data
76
+ company_instance.company_short_name = company_short_name
77
+ company_instance.company = config.get('company')
66
78
 
67
79
  except Exception as e:
68
- logging.error(f"❌ Failed to register configuration for '{company_name}': {e}")
80
+ logging.error(f"❌ Failed to register configuration for '{company_short_name}': {e}")
69
81
  raise e
70
82
 
71
83
  return True
@@ -95,7 +107,13 @@ class Dispatcher:
95
107
 
96
108
  # check if action is a system function using ToolService
97
109
  if self.tool_service.is_system_tool(function_name):
110
+ # this is the system function to be executed.
98
111
  handler = self.tool_service.get_system_handler(function_name)
112
+ logging.info(
113
+ f"Calling system handler [{function_name}] "
114
+ f"with company_short_name={company_short_name} "
115
+ f"and kwargs={kwargs}"
116
+ )
99
117
  return handler(company_short_name, **kwargs)
100
118
 
101
119
  company_instance = self.company_instances[company_short_name]
@@ -74,9 +74,6 @@ class FileProcessor:
74
74
  if not self._apply_filters(file_name):
75
75
  continue
76
76
 
77
- if self.config.echo:
78
- print(f'loading: {file_name}')
79
-
80
77
  content = self.connector.get_file_content(file_path)
81
78
 
82
79
  # execute the callback function
@@ -87,8 +84,6 @@ class FileProcessor:
87
84
  context=self.config.context)
88
85
  self.processed_files += 1
89
86
 
90
- logging.info(f"Successfully processed file: {file_path}")
91
-
92
87
  except Exception as e:
93
88
  logging.error(f"Error processing {file_path}: {e}")
94
89
  if not self.config.continue_on_error:
@@ -1,9 +1,15 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+
1
7
  import logging
2
8
  import json
3
- from typing import Dict, Any, Tuple, Optional
9
+ from typing import Dict, Any, Optional
4
10
  from iatoolkit.services.user_session_context_service import UserSessionContextService
5
11
  from iatoolkit.services.i18n_service import I18nService
6
- from iatoolkit.infra.llm_client import llmClient
12
+ from iatoolkit.services.llm_client_service import llmClient
7
13
  from iatoolkit.repositories.models import Company
8
14
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
9
15
  from iatoolkit.repositories.profile_repo import ProfileRepo
@@ -19,7 +25,7 @@ class HistoryManagerService:
19
25
  3. Database persistence retrieval (full chat history).
20
26
  """
21
27
  TYPE_SERVER_SIDE = 'server_side' # For models like OpenAI
22
- TYPE_CLIENT_SIDE = 'client_side' # For models like Gemini
28
+ TYPE_CLIENT_SIDE = 'client_side' # For models like Gemini and Deepseek
23
29
 
24
30
  GEMINI_MAX_TOKENS_CONTEXT_HISTORY = 200000
25
31
 
@@ -47,7 +53,7 @@ class HistoryManagerService:
47
53
  Initializes a new conversation history.
48
54
  """
49
55
  # 1. Clear existing history
50
- self.session_context.clear_llm_history(company_short_name, user_identifier)
56
+ self.session_context.clear_llm_history(company_short_name, user_identifier, model=model)
51
57
 
52
58
  if history_type == self.TYPE_SERVER_SIDE:
53
59
  # OpenAI: Send system prompt to API and store the resulting ID
@@ -56,14 +62,14 @@ class HistoryManagerService:
56
62
  company_base_context=prepared_context,
57
63
  model=model
58
64
  )
59
- self.session_context.save_last_response_id(company_short_name, user_identifier, response_id)
60
- self.session_context.save_initial_response_id(company_short_name, user_identifier, response_id)
65
+ self.session_context.save_last_response_id(company_short_name, user_identifier, response_id, model=model)
66
+ self.session_context.save_initial_response_id(company_short_name, user_identifier, response_id, model=model)
61
67
  return {'response_id': response_id}
62
68
 
63
69
  elif history_type == self.TYPE_CLIENT_SIDE:
64
70
  # Gemini: Store system prompt as the first message in the list
65
71
  context_history = [{"role": "user", "content": prepared_context}]
66
- self.session_context.save_context_history(company_short_name, user_identifier, context_history)
72
+ self.session_context.save_context_history(company_short_name, user_identifier, context_history, model=model)
67
73
  return {}
68
74
 
69
75
  return {}
@@ -76,14 +82,16 @@ class HistoryManagerService:
76
82
  Populates the request_params within the HistoryHandle.
77
83
  Returns True if a rebuild is needed, False otherwise.
78
84
  """
85
+ model = getattr(handle, "model", None)
86
+
79
87
  if handle.type == self.TYPE_SERVER_SIDE:
80
- previous_response_id = None
81
88
  if ignore_history:
82
- previous_response_id = self.session_context.get_initial_response_id(handle.company_short_name,
83
- handle.user_identifier)
89
+ previous_response_id = self.session_context.get_initial_response_id(
90
+ handle.company_short_name,handle.user_identifier,model=model)
84
91
  else:
85
- previous_response_id = self.session_context.get_last_response_id(handle.company_short_name,
86
- handle.user_identifier)
92
+ previous_response_id = self.session_context.get_last_response_id(
93
+ handle.company_short_name,handle.user_identifier,model=model)
94
+
87
95
 
88
96
  if not previous_response_id:
89
97
  handle.request_params = {}
@@ -93,8 +101,8 @@ class HistoryManagerService:
93
101
  return False
94
102
 
95
103
  elif handle.type == self.TYPE_CLIENT_SIDE:
96
- context_history = self.session_context.get_context_history(handle.company_short_name,
97
- handle.user_identifier) or []
104
+ context_history = self.session_context.get_context_history(
105
+ handle.company_short_name,handle.user_identifier,model=model) or []
98
106
 
99
107
  if not context_history:
100
108
  handle.request_params = {}
@@ -104,7 +112,7 @@ class HistoryManagerService:
104
112
  # Keep only system prompt
105
113
  context_history = [context_history[0]]
106
114
 
107
- # For Gemini, we append the current user turn to the context sent to the API
115
+ # Append the current user turn to the context sent to the API
108
116
  context_history.append({"role": "user", "content": user_turn_prompt})
109
117
 
110
118
  self._trim_context_history(context_history)
@@ -125,15 +133,23 @@ class HistoryManagerService:
125
133
  history_type = history_handle.type
126
134
  company_short_name = history_handle.company_short_name
127
135
  user_identifier = history_handle.user_identifier
136
+ model = getattr(history_handle, "model", None)
128
137
 
129
138
  if history_type == self.TYPE_SERVER_SIDE:
130
139
  if "response_id" in response:
131
- self.session_context.save_last_response_id(company_short_name, user_identifier,
132
- response["response_id"])
140
+ self.session_context.save_last_response_id(
141
+ company_short_name,
142
+ user_identifier,
143
+ response["response_id"],
144
+ model=model)
133
145
 
134
146
  elif history_type == self.TYPE_CLIENT_SIDE:
135
- context_history = self.session_context.get_context_history(company_short_name,
136
- user_identifier) or []
147
+ # get the history for this company/user/model
148
+ context_history = self.session_context.get_context_history(
149
+ company_short_name,
150
+ user_identifier,
151
+ model=model)
152
+
137
153
  # Ensure the user prompt is recorded if not already.
138
154
  # We check content equality to handle the case where the previous message was
139
155
  # also 'user' (e.g., System Prompt) but different content.
@@ -142,10 +158,14 @@ class HistoryManagerService:
142
158
  if last_content != user_turn_prompt:
143
159
  context_history.append({"role": "user", "content": user_turn_prompt})
144
160
 
145
- if response.get('output'):
146
- context_history.append({"role": "model", "content": response['output']})
161
+ if response.get('answer'):
162
+ context_history.append({"role": "assistant", "content": response.get('answer', '')})
147
163
 
148
- self.session_context.save_context_history(company_short_name, user_identifier, context_history)
164
+ self.session_context.save_context_history(
165
+ company_short_name,
166
+ user_identifier,
167
+ context_history,
168
+ model=model)
149
169
 
150
170
  def _trim_context_history(self, context_history: list):
151
171
  """Internal helper to keep token usage within limits for client-side history."""
@@ -169,8 +189,7 @@ class HistoryManagerService:
169
189
  except IndexError:
170
190
  break
171
191
 
172
- # --- Database History Management (Legacy HistoryService) ---
173
-
192
+ # --- this is for the history popup in the chat page
174
193
  def get_full_history(self, company_short_name: str, user_identifier: str) -> dict:
175
194
  """Retrieves the full persisted history from the database."""
176
195
  try: