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
@@ -0,0 +1,412 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+
7
+ from iatoolkit.repositories.models import Document, VSDoc, Company, DocumentStatus
8
+ from iatoolkit.repositories.document_repo import DocumentRepo
9
+ from iatoolkit.repositories.vs_repo import VSRepo
10
+ from iatoolkit.repositories.models import CollectionType
11
+ from iatoolkit.services.document_service import DocumentService
12
+ from iatoolkit.services.profile_service import ProfileService
13
+ from iatoolkit.services.i18n_service import I18nService
14
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
15
+ from sqlalchemy import desc
16
+ from typing import Dict
17
+ from iatoolkit.common.exceptions import IAToolkitException
18
+ import base64
19
+ import logging
20
+ import hashlib
21
+ from typing import List, Optional, Union
22
+ from datetime import datetime
23
+ from injector import inject
24
+
25
+
26
+ class KnowledgeBaseService:
27
+ """
28
+ Central service for managing the RAG (Retrieval-Augmented Generation) Knowledge Base.
29
+ Orchestrates ingestion (OCR -> Split -> Embed -> Store), retrieval, and management.
30
+ """
31
+
32
+ @inject
33
+ def __init__(self,
34
+ document_repo: DocumentRepo,
35
+ vs_repo: VSRepo,
36
+ document_service: DocumentService,
37
+ profile_service: ProfileService,
38
+ i18n_service: I18nService):
39
+ self.document_repo = document_repo
40
+ self.vs_repo = vs_repo
41
+ self.document_service = document_service
42
+ self.profile_service = profile_service
43
+ self.i18n_service = i18n_service
44
+
45
+ # Configure LangChain for intelligent text splitting
46
+ self.text_splitter = RecursiveCharacterTextSplitter(
47
+ chunk_size=1000,
48
+ chunk_overlap=100,
49
+ separators=["\n\n", "\n", ".", " ", ""]
50
+ )
51
+
52
+ def ingest_document_sync(self,
53
+ company: Company,
54
+ filename: str,
55
+ content: bytes,
56
+ user_identifier: str = None,
57
+ metadata: dict = None,
58
+ collection: str = None) -> Document:
59
+ """
60
+ Synchronously processes a document through the entire RAG pipeline:
61
+ 1. Saves initial metadata and raw content (base64) to the SQL Document table.
62
+ 2. Extracts text using DocumentService (handles OCR, PDF, DOCX).
63
+ 3. Splits the text into semantic chunks using LangChain.
64
+ 4. Vectorizes and saves chunks to the Vector Store (VSRepo).
65
+ 5. Updates the document status to ACTIVE or FAILED.
66
+
67
+ Args:
68
+ company: The company owning the document.
69
+ filename: Original filename.
70
+ content: Raw bytes of the file.
71
+ metadata: Optional dictionary with additional info (e.g., document_type).
72
+
73
+ Returns:
74
+ The created Document object.
75
+ """
76
+ if not metadata:
77
+ metadata = {}
78
+
79
+ # --- Logic for Collection ---
80
+ # priority: 1. method parameter 2. metadata
81
+ collection_name = collection or metadata.get('collection')
82
+ collection_type_id = self._get_collection_type_id(company.id, collection_name)
83
+
84
+ # 1. Calculate SHA-256 hash of the content
85
+ file_hash = hashlib.sha256(content).hexdigest()
86
+
87
+ # 2. Check for duplicates by HASH (Content deduplication)
88
+ # If the same content exists (even with a different filename), we skip processing.
89
+ existing_doc = self.document_repo.get_by_hash(company.id, file_hash)
90
+ if existing_doc:
91
+ msg = self.i18n_service.t('rag.ingestion.duplicate', filename=filename, company_short_name=company.short_name)
92
+ logging.info(msg)
93
+ return existing_doc
94
+
95
+
96
+ # 3. Create initial record with PENDING status
97
+ try:
98
+ # Encode to b64 for safe storage in DB if needed later for download
99
+ content_b64 = base64.b64encode(content).decode('utf-8')
100
+
101
+ new_doc = Document(
102
+ company_id=company.id,
103
+ collection_type_id=collection_type_id,
104
+ filename=filename,
105
+ hash=file_hash,
106
+ user_identifier=user_identifier,
107
+ content="", # Will be populated after text extraction
108
+ content_b64=content_b64,
109
+ meta=metadata,
110
+ status=DocumentStatus.PENDING
111
+ )
112
+
113
+ self.document_repo.insert(new_doc)
114
+
115
+ # 3. Start processing (Extraction + Vectorization)
116
+ self._process_document_content(company.short_name, new_doc, content)
117
+
118
+ return new_doc
119
+
120
+ except Exception as e:
121
+ logging.exception(f"Error initializing document ingestion for {filename}: {e}")
122
+ error_msg = self.i18n_service.t('rag.ingestion.failed', error=str(e))
123
+
124
+ raise IAToolkitException(IAToolkitException.ErrorType.LOAD_DOCUMENT_ERROR, error_msg)
125
+
126
+
127
+ def _process_document_content(self, company_short_name: str, document: Document, raw_content: bytes):
128
+ """
129
+ Internal method to handle the heavy lifting of extraction and vectorization.
130
+ Updates the document status directly via the session.
131
+ """
132
+ session = self.document_repo.session
133
+
134
+ try:
135
+ # A. Update status to PROCESSING
136
+ document.status = DocumentStatus.PROCESSING
137
+ session.commit()
138
+
139
+ # B. Text Extraction (Uses existing service logic for OCR, etc.)
140
+ extracted_text = self.document_service.file_to_txt(document.filename, raw_content)
141
+
142
+ if not extracted_text:
143
+ raise ValueError(self.i18n_service.t('rag.ingestion.empty_text'))
144
+
145
+ # Update the extracted content in the original document record
146
+ document.content = extracted_text
147
+
148
+ # C. Splitting (LangChain)
149
+ chunks = self.text_splitter.split_text(extracted_text)
150
+
151
+ # D. Create VSDocs (Chunks)
152
+ # Note: The embedding generation happens inside VSRepo or can be explicit here
153
+ vs_docs = []
154
+ for chunk_text in chunks:
155
+ vs_doc = VSDoc(
156
+ company_id=document.company_id,
157
+ document_id=document.id,
158
+ text=chunk_text
159
+ )
160
+ vs_docs.append(vs_doc)
161
+
162
+ # E. Vector Storage
163
+ # We need the short_name so VSRepo knows which API Key to use for embeddings
164
+ self.vs_repo.add_document(company_short_name, vs_docs)
165
+
166
+ # F. Finalize
167
+ document.status = DocumentStatus.ACTIVE
168
+ session.commit()
169
+ logging.info(f"Successfully ingested {document.description} with {len(chunks)} chunks.")
170
+
171
+ except Exception as e:
172
+ session.rollback()
173
+ logging.error(f"Failed to process document {document.id}: {e}")
174
+
175
+ # Attempt to save the error state
176
+ try:
177
+ document.status = DocumentStatus.FAILED
178
+ document.error_message = str(e)
179
+ session.commit()
180
+ except:
181
+ pass # If error commit fails, we can't do much more
182
+
183
+ error_msg = self.i18n_service.t('rag.ingestion.processing_failed', error=str(e))
184
+ raise IAToolkitException(IAToolkitException.ErrorType.LOAD_DOCUMENT_ERROR, error_msg)
185
+
186
+
187
+ def search(self, company_short_name: str, query: str, n_results: int = 5, metadata_filter: dict = None) -> str:
188
+ """
189
+ Performs a semantic search against the vector store and formats the result as a context string for LLMs.
190
+ Replaces the legacy SearchService logic.
191
+
192
+ Args:
193
+ company_short_name: The target company.
194
+ query: The user's question or search term.
195
+ n_results: Max number of chunks to retrieve.
196
+ metadata_filter: Optional filter for document metadata.
197
+
198
+ Returns:
199
+ Formatted string with context.
200
+ """
201
+ company = self.profile_service.get_company_by_short_name(company_short_name)
202
+ if not company:
203
+ return f"error: {self.i18n_service.t('rag.search.company_not_found', company_short_name=company_short_name)}"
204
+
205
+ # Queries VSRepo (which typically uses pgvector/SQL underneath)
206
+ chunk_list = self.vs_repo.query(
207
+ company_short_name=company_short_name,
208
+ query_text=query,
209
+ n_results=n_results,
210
+ metadata_filter=metadata_filter
211
+ )
212
+
213
+ search_context = ''
214
+ for chunk in chunk_list:
215
+ # 'doc' here is a reconstructed Document object containing the chunk text
216
+ search_context += f'document "{chunk['filename']}"'
217
+
218
+ if chunk.get('meta') and 'document_type' in chunk.get('meta'):
219
+ doc_type = chunk.get('meta').get('document_type', '')
220
+ search_context += f' type: {doc_type}'
221
+
222
+ search_context += f': {chunk.get('text')}\n\n'
223
+
224
+ return search_context
225
+
226
+ def search_raw(self,
227
+ company_short_name: str,
228
+ query: str, n_results: int = 5,
229
+ collection: str = None,
230
+ metadata_filter: dict = None
231
+ ) -> List[Dict]:
232
+ """
233
+ Performs a semantic search and returns the list of Document objects (chunks).
234
+ Useful for UI displays where structured data is needed instead of a raw string context.
235
+
236
+ Args:
237
+ company_short_name: The target company.
238
+ query: The user's question or search term.
239
+ n_results: Max number of chunks to retrieve.
240
+ metadata_filter: Optional filter for document metadata.
241
+
242
+ Returns:
243
+ List of Document objects found.
244
+ """
245
+ company = self.profile_service.get_company_by_short_name(company_short_name)
246
+ if not company:
247
+ # We return empty list instead of error string for consistency
248
+ logging.warning(f"Company {company_short_name} not found during raw search.")
249
+ return []
250
+
251
+ # If collection name provided, resolve to ID or handle in VSRepo
252
+ collection_id = None
253
+ if collection:
254
+ collection_id = self._get_collection_type_id(company.id, collection)
255
+ if not collection_id:
256
+ logging.warning(f"Collection '{collection}' not found. Searching all.")
257
+
258
+
259
+ # Queries VSRepo directly
260
+ chunk_list = self.vs_repo.query(
261
+ company_short_name=company_short_name,
262
+ query_text=query,
263
+ n_results=n_results,
264
+ metadata_filter=metadata_filter,
265
+ collection_id=collection_id,
266
+ )
267
+
268
+ return chunk_list
269
+
270
+ def list_documents(self,
271
+ company_short_name: str,
272
+ status: Optional[Union[str, List[str]]] = None,
273
+ user_identifier: Optional[str] = None,
274
+ collection: str = None,
275
+ filename_keyword: Optional[str] = None,
276
+ from_date: Optional[datetime] = None,
277
+ to_date: Optional[datetime] = None,
278
+ limit: int = 100,
279
+ offset: int = 0) -> List[Document]:
280
+ """
281
+ Retrieves a paginated list of documents based on various filters.
282
+ Used by the frontend to display the Knowledge Base grid.
283
+
284
+ Args:
285
+ company_short_name: Required. Filters by company.
286
+ status: Optional status enum value or list of values (e.g. 'active' or ['active', 'failed']).
287
+ user_identifier: Optional. Filters by the user who uploaded the document.
288
+ filename_keyword: Optional substring to search in filename.
289
+ from_date: Optional start date filter (created_at).
290
+ to_date: Optional end date filter (created_at).
291
+ limit: Pagination limit.
292
+ offset: Pagination offset.
293
+
294
+ Returns:
295
+ List of Document objects matching the criteria.
296
+ """
297
+ session = self.document_repo.session
298
+
299
+ # Start building the query
300
+ query = session.query(Document).join(Company).filter(Company.short_name == company_short_name)
301
+
302
+ # Filter by status (single string or list)
303
+ if status:
304
+ if isinstance(status, list):
305
+ query = query.filter(Document.status.in_(status))
306
+ else:
307
+ query = query.filter(Document.status == status)
308
+
309
+ # filter by collection
310
+ if collection:
311
+ query = query.join(CollectionType).filter(CollectionType.name == collection)
312
+
313
+ # Filter by user identifier
314
+ if user_identifier:
315
+ query = query.filter(Document.user_identifier.ilike(f"%{user_identifier}%"))
316
+
317
+ if filename_keyword:
318
+ # Case-insensitive search
319
+ query = query.filter(Document.filename.ilike(f"%{filename_keyword}%"))
320
+
321
+ if from_date:
322
+ query = query.filter(Document.created_at >= from_date)
323
+
324
+ if to_date:
325
+ query = query.filter(Document.created_at <= to_date)
326
+
327
+ # Apply sorting (newest first) and pagination
328
+ query = query.order_by(desc(Document.created_at))
329
+ query = query.limit(limit).offset(offset)
330
+
331
+ return query.all()
332
+
333
+ def get_document_content(self, document_id: int) -> tuple[bytes, str]:
334
+ """
335
+ Retrieves the raw content of a document and its filename.
336
+
337
+ Args:
338
+ document_id: ID of the document.
339
+
340
+ Returns:
341
+ A tuple containing (file_bytes, filename).
342
+ Returns (None, None) if document not found.
343
+ """
344
+ doc = self.document_repo.get_by_id(document_id)
345
+ if not doc or not doc.content_b64:
346
+ return None, None
347
+
348
+ try:
349
+ file_bytes = base64.b64decode(doc.content_b64)
350
+ return file_bytes, doc.filename
351
+ except Exception as e:
352
+ logging.error(f"Error decoding content for document {document_id}: {e}")
353
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_FORMAT_ERROR,
354
+ f"Error reading file content: {e}")
355
+
356
+ def delete_document(self, document_id: int) -> bool:
357
+ """
358
+ Deletes a document and its associated vectors.
359
+ Since vectors are linked via FK with ON DELETE CASCADE, deleting the Document record is sufficient.
360
+
361
+ Args:
362
+ document_id: The ID of the document to delete.
363
+
364
+ Returns:
365
+ True if deleted, False if not found.
366
+ """
367
+ doc = self.document_repo.get_by_id(document_id)
368
+ if not doc:
369
+ return False
370
+
371
+ session = self.document_repo.session
372
+ try:
373
+ session.delete(doc)
374
+ session.commit()
375
+ return True
376
+ except Exception as e:
377
+ session.rollback()
378
+ logging.error(f"Error deleting document {document_id}: {e}")
379
+ raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
380
+ f"Error deleting document: {e}")
381
+
382
+ def sync_collection_types(self, company_short_name: str, categories_config: list):
383
+ """
384
+ This should be called during company initialization or configuration reload.
385
+ """
386
+ company = self.profile_service.get_company_by_short_name(company_short_name)
387
+ if not company:
388
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
389
+ f'Company {company_short_name} not found')
390
+
391
+
392
+ session = self.document_repo.session
393
+ existing_types = session.query(CollectionType).filter_by(company_id=company.id).all()
394
+ existing_names = {ct.name: ct for ct in existing_types}
395
+
396
+ for cat_name in categories_config:
397
+ if cat_name not in existing_names:
398
+ new_type = CollectionType(company_id=company.id, name=cat_name)
399
+ session.add(new_type)
400
+
401
+ # Opcional: Eliminar los que ya no están en el config?
402
+ # Por seguridad de datos, mejor no borrar automáticamente, o marcarlos inactivos.
403
+
404
+ session.commit()
405
+
406
+ def _get_collection_type_id(self, company_id: int, collection_name: str) -> Optional[int]:
407
+ """Helper to get ID by name"""
408
+ if not collection_name:
409
+ return None
410
+ session = self.document_repo.session
411
+ ct = session.query(CollectionType).filter_by(company_id=company_id, name=collection_name).first()
412
+ return ct.id if ct else None
@@ -8,6 +8,7 @@ from iatoolkit.repositories.models import Company, LLMQuery
8
8
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
9
9
  from sqlalchemy.exc import SQLAlchemyError, OperationalError
10
10
  from iatoolkit.common.util import Utility
11
+ from iatoolkit.common.model_registry import ModelRegistry
11
12
  from injector import inject
12
13
  import time
13
14
  import markdown2
@@ -30,11 +31,13 @@ class llmClient:
30
31
  @inject
31
32
  def __init__(self,
32
33
  llmquery_repo: LLMQueryRepo,
33
- llm_proxy_factory: LLMProxy,
34
+ llm_proxy: LLMProxy,
35
+ model_registry: ModelRegistry,
34
36
  util: Utility
35
37
  ):
36
38
  self.llmquery_repo = llmquery_repo
37
- self.llm_proxy_factory = llm_proxy_factory
39
+ self.llm_proxy = llm_proxy
40
+ self.model_registry = model_registry
38
41
  self.util = util
39
42
  self._dispatcher = None # Cache for the lazy-loaded dispatcher
40
43
 
@@ -73,21 +76,15 @@ class llmClient:
73
76
  response = None
74
77
  sql_retry_count = 0
75
78
  force_tool_name = None
76
- reasoning = {}
77
79
 
78
- if model in ('gpt-5', 'gpt-5-mini'):
79
- text['verbosity'] = "low"
80
- reasoning = {"effort": 'minimal'}
81
- elif model == 'gpt-5.1':
82
- text['verbosity'] = "low"
83
- reasoning = {"effort": 'low'}
80
+ # Resolve per-model defaults and apply overrides (without mutating inputs).
81
+ request_params = self.model_registry.resolve_request_params(model=model, text=text)
82
+ text_payload = request_params["text"]
83
+ reasoning = request_params["reasoning"]
84
84
 
85
85
  try:
86
86
  start_time = time.time()
87
- logging.info(f"calling llm model '{model}' with {self.count_tokens(context)} tokens...")
88
-
89
- # get the proxy for the company
90
- llm_proxy = self.llm_proxy_factory.create_for_company(company)
87
+ logging.info(f"calling llm model '{model}' with {self.count_tokens(context, context_history)} tokens...")
91
88
 
92
89
  # this is the first call to the LLM on the iteration
93
90
  try:
@@ -96,13 +93,14 @@ class llmClient:
96
93
  "content": context
97
94
  }]
98
95
 
99
- response = llm_proxy.create_response(
96
+ response = self.llm_proxy.create_response(
97
+ company_short_name=company.short_name,
100
98
  model=model,
99
+ input=input_messages,
101
100
  previous_response_id=previous_response_id,
102
101
  context_history=context_history,
103
- input=input_messages,
104
102
  tools=tools,
105
- text=text,
103
+ text=text_payload,
106
104
  reasoning=reasoning,
107
105
  )
108
106
  stats = self.get_stats(response)
@@ -130,8 +128,14 @@ class llmClient:
130
128
  # execute the function call through the dispatcher
131
129
  fcall_time = time.time()
132
130
  function_name = tool_call.name
133
- args = json.loads(tool_call.arguments)
134
- logging.info(f"start execution fcall: {function_name}")
131
+
132
+ try:
133
+ args = json.loads(tool_call.arguments)
134
+ except Exception as e:
135
+ logging.error(f"[Dispatcher] json.loads failed: {e}")
136
+ raise
137
+ logging.debug(f"[Dispatcher] Parsed args = {args}")
138
+
135
139
  try:
136
140
  result = self.dispatcher.dispatch(
137
141
  company_short_name=company.short_name,
@@ -163,6 +167,7 @@ class llmClient:
163
167
  input_messages.append({
164
168
  "type": "function_call_output",
165
169
  "call_id": tool_call.call_id,
170
+ "status": "completed",
166
171
  "output": str(result)
167
172
  })
168
173
  function_calls = True
@@ -173,7 +178,7 @@ class llmClient:
173
178
  f_calls.append(f_call_identity)
174
179
  f_call_time += elapsed
175
180
 
176
- logging.info(f"[{company.short_name}] Tool end execution: {function_name} in {elapsed:.1f} secs.")
181
+ logging.info(f"[{company.short_name}] end execution of tool: {function_name} in {elapsed:.1f} secs.")
177
182
 
178
183
  if not function_calls:
179
184
  break # no more function calls, the answer to send back to llm
@@ -183,7 +188,8 @@ class llmClient:
183
188
  if force_tool_name:
184
189
  tool_choice_value = "required"
185
190
 
186
- response = llm_proxy.create_response(
191
+ response = self.llm_proxy.create_response(
192
+ company_short_name=company.short_name,
187
193
  model=model,
188
194
  input=input_messages,
189
195
  previous_response_id=response.id,
@@ -191,7 +197,7 @@ class llmClient:
191
197
  reasoning=reasoning,
192
198
  tool_choice=tool_choice_value,
193
199
  tools=tools,
194
- text=text
200
+ text=text_payload,
195
201
  )
196
202
  stats_fcall = self.add_stats(stats_fcall, self.get_stats(response))
197
203
 
@@ -203,9 +209,11 @@ class llmClient:
203
209
  # decode the LLM response
204
210
  decoded_response = self.decode_response(response)
205
211
 
212
+ # Extract reasoning from the final response object
213
+ final_reasoning = getattr(response, 'reasoning_content', '')
214
+
206
215
  # save the query and response
207
216
  query = LLMQuery(user_identifier=user_identifier,
208
- task_id=0,
209
217
  company_id=company.id,
210
218
  query=question,
211
219
  output=decoded_response.get('answer', ''),
@@ -230,6 +238,7 @@ class llmClient:
230
238
  'response_id': response.id,
231
239
  'query_id': query.id,
232
240
  'model': model,
241
+ 'reasoning_content': final_reasoning,
233
242
  }
234
243
  except SQLAlchemyError as db_error:
235
244
  # rollback
@@ -244,11 +253,10 @@ class llmClient:
244
253
 
245
254
  # log the error in the llm_query table
246
255
  query = LLMQuery(user_identifier=user_identifier,
247
- task_id=0,
248
256
  company_id=company.id,
249
257
  query=question,
250
258
  output=error_message,
251
- response=response.output_text if response else {},
259
+ response={},
252
260
  valid_response=False,
253
261
  function_calls=f_calls,
254
262
  )
@@ -269,14 +277,15 @@ class llmClient:
269
277
 
270
278
  logging.info(f"initializing model '{model}' with company context: {self.count_tokens(company_base_context)} tokens...")
271
279
 
272
- llm_proxy = self.llm_proxy_factory.create_for_company(company)
273
280
  try:
274
- response = llm_proxy.create_response(
281
+ response = self.llm_proxy.create_response(
282
+ company_short_name=company.short_name,
275
283
  model=model,
276
284
  input=[{
277
285
  "role": "system",
278
286
  "content": company_base_context
279
- }]
287
+ }],
288
+
280
289
  )
281
290
 
282
291
  except Exception as e:
@@ -423,7 +432,7 @@ class llmClient:
423
432
  html_answer = markdown2.markdown(answer).replace("\n", "")
424
433
  return html_answer
425
434
 
426
- def count_tokens(self, text):
435
+ def count_tokens(self, text, history = []):
427
436
  # Codifica el texto y cuenta la cantidad de tokens
428
- tokens = self.encoding.encode(text)
437
+ tokens = self.encoding.encode(text + json.dumps(history))
429
438
  return len(tokens)