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,57 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.infra.connectors.file_connector import FileConnector
7
+ from iatoolkit.infra.connectors.local_file_connector import LocalFileConnector
8
+ from iatoolkit.infra.connectors.s3_connector import S3Connector
9
+ from iatoolkit.infra.connectors.google_drive_connector import GoogleDriveConnector
10
+ from iatoolkit.infra.connectors.google_cloud_storage_connector import GoogleCloudStorageConnector
11
+ from typing import Dict
12
+ import os
13
+
14
+
15
+ class FileConnectorFactory:
16
+ @staticmethod
17
+ def create(config: Dict) -> FileConnector:
18
+ """
19
+ Configuración esperada:
20
+ {
21
+ "type": "local" | "s3" | "gdrive" | "gcs",
22
+ "path": "/ruta/local", # solo para local
23
+ "bucket": "mi-bucket", "prefix": "datos/", "auth": {...}, # solo para S3
24
+ "folder_id": "xxxxxxx", # solo para Google Drive
25
+ "bucket": "mi-bucket", "service_account": "/ruta/service_account.json" # solo para GCS
26
+ }
27
+ """
28
+ connector_type = config.get('type')
29
+
30
+ if connector_type == 'local':
31
+ return LocalFileConnector(config['path'])
32
+
33
+ elif connector_type == 's3':
34
+ auth = {
35
+ 'aws_access_key_id': os.getenv('AWS_ACCESS_KEY_ID'),
36
+ 'aws_secret_access_key': os.getenv('AWS_SECRET_ACCESS_KEY'),
37
+ 'region_name': os.getenv('AWS_REGION', 'us-east-1')
38
+ }
39
+
40
+ return S3Connector(
41
+ bucket=config['bucket'],
42
+ prefix=config.get('prefix', ''),
43
+ auth=auth
44
+ )
45
+
46
+ elif connector_type == 'gdrive':
47
+ return GoogleDriveConnector(config['folder_id'],
48
+ 'service_account.json')
49
+
50
+ elif connector_type == 'gcs':
51
+ return GoogleCloudStorageConnector(
52
+ bucket_name=config['bucket'],
53
+ service_account_path=config.get('service_account', 'service_account.json')
54
+ )
55
+
56
+ else:
57
+ raise ValueError(f"Unknown connector type: {connector_type}")
@@ -0,0 +1,53 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.infra.connectors.file_connector import FileConnector
7
+ from google.cloud import storage
8
+ from typing import List
9
+
10
+
11
+ class GoogleCloudStorageConnector(FileConnector):
12
+ def __init__(self, bucket_name: str, service_account_path: str = "service_account.json"):
13
+ """
14
+ Inicializa el conector de Google Cloud Storage utilizando la API oficial de Google.
15
+ :param bucket_name: Nombre del bucket en Google Cloud Storage.
16
+ :param service_account_path: Ruta al archivo JSON de la cuenta de servicio.
17
+ """
18
+ self.bucket_name = bucket_name
19
+ self.service_account_path = service_account_path
20
+ self.storage_client = self._authenticate()
21
+ self.bucket = self.storage_client.bucket(bucket_name)
22
+
23
+ def _authenticate(self):
24
+ """
25
+ Autentica en Google Cloud Storage utilizando una cuenta de servicio.
26
+ """
27
+ # Crear cliente de GCS con las credenciales
28
+ client = storage.Client.from_service_account_json(self.service_account_path)
29
+ return client
30
+
31
+ def list_files(self) -> List[dict]:
32
+ """
33
+ Lista todos los archivos en el bucket de GCS como diccionarios con claves 'path', 'name' y 'metadata'.
34
+ """
35
+ blobs = self.bucket.list_blobs()
36
+
37
+ return [
38
+ {
39
+ "path": blob.name, # Nombre o "ruta" del blob en el bucket
40
+ "name": blob.name.split("/")[-1], # Nombre del archivo (última parte del path)
41
+ "metadata": {"size": blob.size} # Incluye tamaño como metadata (u otros metadatos relevantes)
42
+ }
43
+ for blob in blobs
44
+ ]
45
+
46
+ def get_file_content(self, file_path: str) -> bytes:
47
+ """
48
+ Descarga el contenido de un archivo en GCS dado su path (nombre del blob).
49
+ """
50
+ blob = self.bucket.blob(file_path)
51
+ file_buffer = blob.download_as_bytes() # Descarga el contenido como bytes
52
+
53
+ return file_buffer
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.infra.connectors.file_connector import FileConnector
7
+ from googleapiclient.discovery import build
8
+ from googleapiclient.http import MediaIoBaseDownload
9
+ from google.oauth2.service_account import Credentials
10
+ import io
11
+ from typing import List
12
+
13
+
14
+ class GoogleDriveConnector(FileConnector):
15
+ def __init__(self, folder_id: str, service_account_path: str = "service_account.json"):
16
+ """
17
+ Inicializa el conector de Google Drive utilizando la API oficial de Google.
18
+ :param folder_id: ID de la carpeta en Google Drive.
19
+ :param service_account_path: Ruta al archivo JSON de la cuenta de servicio.
20
+ """
21
+ self.folder_id = folder_id
22
+ self.service_account_path = service_account_path
23
+ self.drive_service = self._authenticate()
24
+
25
+ def _authenticate(self):
26
+ """
27
+ Autentica en Google Drive utilizando una cuenta de servicio.
28
+ """
29
+ # Cargar credenciales desde el archivo de servicio
30
+ credentials = Credentials.from_service_account_file(
31
+ self.service_account_path,
32
+ scopes=["https://www.googleapis.com/auth/drive"]
33
+ )
34
+
35
+ # Crear el cliente de Google Drive API
36
+ service = build('drive', 'v3', credentials=credentials)
37
+ return service
38
+
39
+ def list_files(self) -> List[dict]:
40
+ """
41
+ Estándar: Lista todos los archivos como diccionarios con claves 'path', 'name' y 'metadata'.
42
+ """
43
+ query = f"'{self.folder_id}' in parents and trashed=false"
44
+ results = self.drive_service.files().list(q=query, fields="files(id, name)").execute()
45
+ files = results.get('files', [])
46
+
47
+ return [
48
+ {
49
+ "path": file['id'], # ID único del archivo en Google Drive
50
+ "name": file['name'], # Nombre del archivo en Google Drive
51
+ "metadata": {} # No hay metadatos adicionales en este caso
52
+ }
53
+ for file in files
54
+ ]
55
+
56
+ def get_file_content(self, file_path: str) -> bytes:
57
+ """
58
+ Obtiene el contenido de un archivo en Google Drive utilizando su ID (file_path).
59
+ """
60
+ request = self.drive_service.files().get_media(fileId=file_path)
61
+ file_buffer = io.BytesIO()
62
+ downloader = MediaIoBaseDownload(file_buffer, request)
63
+
64
+ done = False
65
+ while not done:
66
+ status, done = downloader.next_chunk()
67
+
68
+ return file_buffer.getvalue()
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import os
7
+ from iatoolkit.infra.connectors.file_connector import FileConnector
8
+ from typing import List
9
+ from iatoolkit.common.exceptions import IAToolkitException
10
+
11
+
12
+ class LocalFileConnector(FileConnector):
13
+ def __init__(self, directory: str):
14
+ local_root = os.getenv("ROOT_DIR_LOCAL_FILES", '')
15
+ self.directory = os.path.join(local_root, directory)
16
+
17
+ def list_files(self) -> List[dict]:
18
+ """
19
+ Estándar: Lista todos los archivos como diccionarios con claves 'path', 'name' y 'metadata'.
20
+ """
21
+ try:
22
+ files = [
23
+ os.path.join(self.directory, f)
24
+ for f in os.listdir(self.directory)
25
+ if os.path.isfile(os.path.join(self.directory, f))
26
+ ]
27
+
28
+ return [
29
+ {
30
+ "path": file, # Ruta completa al archivo local
31
+ "name": os.path.basename(file), # Nombre del archivo
32
+ "metadata": {"size": os.path.getsize(file), "last_modified": os.path.getmtime(file)}
33
+ }
34
+ for file in files
35
+ ]
36
+ except Exception as e:
37
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
38
+ f"Error procesando el directorio {self.directory}: {e}")
39
+
40
+ def get_file_content(self, file_path: str) -> bytes:
41
+ try:
42
+ with open(file_path, 'rb') as f:
43
+ return f.read()
44
+ except Exception as e:
45
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
46
+ f"Error leyendo el archivo {file_path}: {e}")
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import boto3
7
+ from iatoolkit.infra.connectors.file_connector import FileConnector
8
+ from typing import List
9
+
10
+
11
+ class S3Connector(FileConnector):
12
+ def __init__(self, bucket: str, prefix: str, auth: dict):
13
+ self.bucket = bucket
14
+ self.prefix = prefix
15
+ self.s3 = boto3.client('s3', **auth)
16
+
17
+ def list_files(self) -> List[dict]:
18
+ # list all the files as dictionaries, with keys: 'path', 'name' y 'metadata'.
19
+ response = self.s3.list_objects_v2(Bucket=self.bucket, Prefix=self.prefix)
20
+ files = response.get('Contents', [])
21
+
22
+ return [
23
+ {
24
+ "path": obj['Key'], # s3 key
25
+ "name": obj['Key'].split('/')[-1], # filename
26
+ "metadata": {"size": obj.get('Size'), "last_modified": obj.get('LastModified')}
27
+ }
28
+ for obj in files
29
+ ]
30
+
31
+ def get_file_content(self, file_path: str) -> bytes:
32
+ response = self.s3.get_object(Bucket=self.bucket, Key=file_path)
33
+ return response['Body'].read()
@@ -0,0 +1,356 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.infra.llm_response import LLMResponse, ToolCall, Usage
7
+ from typing import Dict, List, Optional
8
+ from google.generativeai.types import HarmCategory, HarmBlockThreshold
9
+ from google.protobuf.json_format import MessageToDict
10
+ from iatoolkit.common.exceptions import IAToolkitException
11
+ import logging
12
+ import json
13
+ import uuid
14
+
15
+ class GeminiAdapter:
16
+ """Adaptador para la API de Gemini"""
17
+
18
+ def __init__(self, gemini_client):
19
+ """Inicializar con cliente Gemini ya configurado"""
20
+ self.client = gemini_client
21
+
22
+ # Configuración de seguridad - permitir contenido que podría ser bloqueado por defecto
23
+ self.safety_settings = {
24
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
25
+ HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
26
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
27
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
28
+ }
29
+
30
+ def create_response(self,
31
+ model: str,
32
+ input: List[Dict],
33
+ previous_response_id: Optional[str] = None,
34
+ context_history: Optional[List[Dict]] = None,
35
+ tools: Optional[List[Dict]] = None,
36
+ text: Optional[Dict] = None,
37
+ reasoning: Optional[Dict] = None,
38
+ tool_choice: str = "auto",
39
+ ) -> LLMResponse:
40
+ """Llamada a la API de Gemini y mapeo a estructura común"""
41
+ try:
42
+ # Inicializar el modelo de Gemini usando el cliente configurado
43
+ gemini_model = self.client.GenerativeModel(
44
+ model_name=self._map_model_name(model),
45
+ safety_settings=self.safety_settings
46
+ )
47
+
48
+ # Preparar el contenido para Gemini
49
+ if context_history:
50
+ # Concatenar el historial de conversación con el input actual
51
+ contents = self._prepare_gemini_contents(context_history + input)
52
+ else:
53
+ # Usar solo el input actual si no hay historial
54
+ contents = self._prepare_gemini_contents(input)
55
+
56
+ # Preparar herramientas si están disponibles
57
+ gemini_tools = self._prepare_gemini_tools(tools) if tools else None
58
+
59
+ # Configurar generación
60
+ generation_config = self._prepare_generation_config(text, tool_choice)
61
+
62
+ # Llamar a Gemini
63
+ if gemini_tools:
64
+ # Con herramientas
65
+ response = gemini_model.generate_content(
66
+ contents,
67
+ tools=gemini_tools,
68
+ generation_config=generation_config
69
+ )
70
+ else:
71
+ # Sin herramientas
72
+ response = gemini_model.generate_content(
73
+ contents,
74
+ generation_config=generation_config
75
+ )
76
+
77
+ # map the answer to a common structure
78
+ llm_response = self._map_gemini_response(response, model)
79
+
80
+ # add the model answer to the history
81
+ if context_history and llm_response.output_text:
82
+ context_history.append(
83
+ {
84
+ 'role': 'assistant',
85
+ 'context': llm_response.output_text
86
+ }
87
+ )
88
+
89
+ return llm_response
90
+
91
+ except Exception as e:
92
+ error_message = f"Error calling Gemini API: {str(e)}"
93
+ logging.error(error_message)
94
+
95
+ # handle gemini specific errors
96
+ if "quota" in str(e).lower():
97
+ error_message = "Se ha excedido la cuota de la API de Gemini"
98
+ elif "blocked" in str(e).lower():
99
+ error_message = "El contenido fue bloqueado por las políticas de seguridad de Gemini"
100
+ elif "token" in str(e).lower():
101
+ error_message = "Tu consulta supera el límite de contexto de Gemini"
102
+
103
+ raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
104
+
105
+ # ... rest of the methods keep the same ...
106
+ def _map_model_name(self, model: str) -> str:
107
+ """Mapear nombre del modelo a formato de Gemini"""
108
+ model_mapping = {
109
+ "gemini-pro": "gemini-2.5-pro",
110
+ "gemini": "gemini-2.5-pro",
111
+ "gemini-1.5": "gemini-2.5-pro",
112
+ "gemini-flash": "gemini-1.5-flash",
113
+ "gemini-2.0": "gemini-2.0-flash-exp"
114
+ }
115
+ return model_mapping.get(model.lower(), model)
116
+
117
+ def _prepare_gemini_contents(self, input: List[Dict]) -> List[Dict]:
118
+ """Convertir mensajes de formato OpenAI a formato Gemini"""
119
+ gemini_contents = []
120
+
121
+ for message in input:
122
+ if message.get("role") == "system":
123
+ gemini_contents.append({
124
+ "role": "user",
125
+ "parts": [{"text": f"[INSTRUCCIONES DEL SISTEMA]\n{message.get('content', '')}"}]
126
+ })
127
+ elif message.get("role") == "user":
128
+ gemini_contents.append({
129
+ "role": "user",
130
+ "parts": [{"text": message.get("content", "")}]
131
+ })
132
+ elif message.get("type") == "function_call_output":
133
+ gemini_contents.append({
134
+ "role": "function",
135
+ "parts": [{
136
+ "function_response": {
137
+ "name": "tool_result",
138
+ "response": {"output": message.get("output", "")}
139
+ }
140
+ }]
141
+ })
142
+
143
+ return gemini_contents
144
+
145
+ def _prepare_gemini_tools(self, tools: List[Dict]) -> List[Dict]:
146
+ """Convertir herramientas de formato OpenAI a formato Gemini"""
147
+ if not tools:
148
+ return None
149
+
150
+ function_declarations = []
151
+ for i, tool in enumerate(tools):
152
+ # Verificar estructura básica
153
+ tool_type = tool.get("type")
154
+
155
+ if tool_type != "function":
156
+ logging.warning(f"Herramienta {i} no es de tipo 'function': {tool_type}")
157
+ continue
158
+
159
+ # Extraer datos de la herramienta (estructura plana)
160
+ function_name = tool.get("name")
161
+ function_description = tool.get("description", "")
162
+ function_parameters = tool.get("parameters", {})
163
+
164
+ # Verificar si el nombre existe y no está vacío
165
+ if not function_name or not isinstance(function_name, str) or not function_name.strip():
166
+ logging.error(f"PROBLEMA: Herramienta {i} sin nombre válido")
167
+ continue
168
+
169
+ # Preparar la declaración de función para Gemini
170
+ gemini_function = {
171
+ "name": function_name,
172
+ "description": function_description,
173
+ }
174
+
175
+ # Agregar parámetros si existen y limpiar campos específicos de OpenAI
176
+ if function_parameters:
177
+ clean_parameters = self._clean_openai_specific_fields(function_parameters)
178
+ gemini_function["parameters"] = clean_parameters
179
+
180
+ function_declarations.append(gemini_function)
181
+
182
+ if function_declarations:
183
+ final_tools = [{
184
+ "function_declarations": function_declarations
185
+ }]
186
+
187
+ # Log de la estructura final para debug
188
+ # logging.info("Estructura final de herramientas para Gemini:")
189
+ # logging.info(f"{json.dumps(final_tools, indent=2)}")
190
+
191
+ return final_tools
192
+
193
+ return None
194
+
195
+
196
+ def _clean_openai_specific_fields(self, parameters: Dict) -> Dict:
197
+ """Limpiar campos específicos de OpenAI que Gemini no entiende"""
198
+ clean_params = {}
199
+
200
+ # Campos permitidos por Gemini según su Schema protobuf
201
+ # Estos son los únicos campos que Gemini acepta en sus esquemas
202
+ allowed_fields = {
203
+ "type", # Tipo de datos: string, number, object, array, boolean
204
+ "properties", # Para objetos: define las propiedades
205
+ "required", # Array de propiedades requeridas
206
+ "items", # Para arrays: define el tipo de elementos
207
+ "description", # Descripción del campo
208
+ "enum", # Lista de valores permitidos
209
+ # Gemini NO soporta estos campos comunes de JSON Schema:
210
+ # "pattern", "format", "minimum", "maximum", "minItems", "maxItems",
211
+ # "minLength", "maxLength", "additionalProperties", "strict"
212
+ }
213
+
214
+ for key, value in parameters.items():
215
+ if key in allowed_fields:
216
+ if key == "properties" and isinstance(value, dict):
217
+ # Limpiar recursivamente las propiedades
218
+ clean_props = {}
219
+ for prop_name, prop_def in value.items():
220
+ if isinstance(prop_def, dict):
221
+ clean_props[prop_name] = self._clean_openai_specific_fields(prop_def)
222
+ else:
223
+ clean_props[prop_name] = prop_def
224
+ clean_params[key] = clean_props
225
+ elif key == "items" and isinstance(value, dict):
226
+ # Limpiar recursivamente los items de array
227
+ clean_params[key] = self._clean_openai_specific_fields(value)
228
+ else:
229
+ clean_params[key] = value
230
+ else:
231
+ logging.debug(f"Campo '{key}' removido (no soportado por Gemini)")
232
+
233
+ return clean_params
234
+
235
+ def _prepare_generation_config(self, text: Optional[Dict], tool_choice: str) -> Dict:
236
+ """Preparar configuración de generación para Gemini"""
237
+ config = {"candidate_count": 1}
238
+
239
+ if text:
240
+ if "temperature" in text:
241
+ config["temperature"] = float(text["temperature"])
242
+ if "max_tokens" in text:
243
+ config["max_output_tokens"] = int(text["max_tokens"])
244
+ if "top_p" in text:
245
+ config["top_p"] = float(text["top_p"])
246
+
247
+ return config
248
+
249
+ def _map_gemini_response(self, gemini_response, model: str) -> LLMResponse:
250
+ """Mapear respuesta de Gemini a estructura común"""
251
+ response_id = str(uuid.uuid4())
252
+ output_text = ""
253
+ tool_calls = []
254
+
255
+ if gemini_response.candidates and len(gemini_response.candidates) > 0:
256
+ candidate = gemini_response.candidates[0]
257
+
258
+ for part in candidate.content.parts:
259
+ if hasattr(part, 'text') and part.text:
260
+ output_text += part.text
261
+ elif hasattr(part, 'function_call') and part.function_call:
262
+ func_call = part.function_call
263
+ tool_calls.append(ToolCall(
264
+ call_id=f"call_{uuid.uuid4().hex[:8]}",
265
+ type="function_call",
266
+ name=func_call.name,
267
+ arguments=json.dumps(MessageToDict(func_call._pb).get('args', {}))
268
+ ))
269
+
270
+ # Determinar status
271
+ status = "completed"
272
+ if gemini_response.candidates:
273
+ candidate = gemini_response.candidates[0]
274
+ if hasattr(candidate, 'finish_reason'):
275
+ # Manejar finish_reason tanto como objeto con .name como entero/enum directo
276
+ finish_reason = candidate.finish_reason
277
+
278
+ # Si finish_reason tiene un atributo .name, usarlo
279
+ if hasattr(finish_reason, 'name'):
280
+ finish_reason_name = finish_reason.name
281
+ else:
282
+ # Si es un entero o enum directo, convertirlo a string
283
+ finish_reason_name = str(finish_reason)
284
+
285
+ if finish_reason_name in ["SAFETY", "RECITATION", "3", "4"]: # Agregar valores numéricos también
286
+ status = "blocked"
287
+ elif finish_reason_name in ["MAX_TOKENS", "LENGTH", "2"]: # Agregar valores numéricos también
288
+ status = "length_exceeded"
289
+
290
+ # Calcular usage de tokens
291
+ usage = self._extract_usage_metadata(gemini_response)
292
+
293
+ # Estimación básica si no hay datos de usage
294
+ if usage.total_tokens == 0:
295
+ estimated_output_tokens = len(output_text) // 4
296
+ usage = Usage(
297
+ input_tokens=0,
298
+ output_tokens=estimated_output_tokens,
299
+ total_tokens=estimated_output_tokens
300
+ )
301
+
302
+ return LLMResponse(
303
+ id=response_id,
304
+ model=model,
305
+ status=status,
306
+ output_text=output_text,
307
+ output=tool_calls,
308
+ usage=usage
309
+ )
310
+
311
+ def _extract_usage_metadata(self, gemini_response) -> Usage:
312
+ """Extraer información de uso de tokens de manera segura"""
313
+ input_tokens = 0
314
+ output_tokens = 0
315
+ total_tokens = 0
316
+
317
+ try:
318
+ # Verificar si existe usage_metadata
319
+ if hasattr(gemini_response, 'usage_metadata') and gemini_response.usage_metadata:
320
+ usage_metadata = gemini_response.usage_metadata
321
+
322
+ # Acceder a los atributos directamente, no con .get()
323
+ if hasattr(usage_metadata, 'prompt_token_count'):
324
+ input_tokens = usage_metadata.prompt_token_count
325
+ if hasattr(usage_metadata, 'candidates_token_count'):
326
+ output_tokens = usage_metadata.candidates_token_count
327
+ if hasattr(usage_metadata, 'total_token_count'):
328
+ total_tokens = usage_metadata.total_token_count
329
+
330
+ except Exception as e:
331
+ logging.warning(f"No se pudo extraer usage_metadata de Gemini: {e}")
332
+
333
+ # Si no hay datos de usage o son cero, hacer estimación básica
334
+ if total_tokens == 0 and output_tokens == 0:
335
+ # Obtener texto de salida para estimación
336
+ output_text = ""
337
+ if (hasattr(gemini_response, 'candidates') and
338
+ gemini_response.candidates and
339
+ len(gemini_response.candidates) > 0):
340
+
341
+ candidate = gemini_response.candidates[0]
342
+ if hasattr(candidate, 'content') and hasattr(candidate.content, 'parts'):
343
+ for part in candidate.content.parts:
344
+ if hasattr(part, 'text') and part.text:
345
+ output_text += part.text
346
+
347
+ # Estimación básica (4 caracteres por token aproximadamente)
348
+ estimated_output_tokens = len(output_text) // 4 if output_text else 0
349
+ output_tokens = estimated_output_tokens
350
+ total_tokens = estimated_output_tokens
351
+
352
+ return Usage(
353
+ input_tokens=input_tokens,
354
+ output_tokens=output_tokens,
355
+ total_tokens=total_tokens
356
+ )
@@ -0,0 +1,57 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from injector import inject
7
+ from iatoolkit.infra.call_service import CallServiceClient
8
+ import logging
9
+ import os
10
+ from typing import Dict, Any
11
+
12
+
13
+ class GoogleChatApp:
14
+ @inject
15
+ def __init__(self, call_service: CallServiceClient):
16
+ self.call_service = call_service
17
+
18
+ def send_message(self, message_data: Dict[str, Any]) -> Dict[str, Any]:
19
+ """
20
+ Sends a message to Google Chat.
21
+
22
+ Args:
23
+ message_data: Complete message data structure with type, space, and message
24
+
25
+ Returns:
26
+ Dict with the service response
27
+ """
28
+ try:
29
+ # get the bot URL from environment variables
30
+ bot_url = os.getenv('GOOGLE_CHAT_BOT_URL')
31
+ if not bot_url:
32
+ raise Exception('GOOGLE_CHAT_BOT_URL no está configurada en las variables de entorno')
33
+
34
+ # send the POST request with the complete message data
35
+ response, status_code = self.call_service.post(bot_url, message_data)
36
+
37
+ if status_code == 200:
38
+ return {
39
+ "success": True,
40
+ "message": "Mensaje enviado correctamente",
41
+ "response": response
42
+ }
43
+ else:
44
+ logging.error(f"Error al enviar mensaje a Google Chat. Status: {status_code}, Response: {response}")
45
+ return {
46
+ "success": False,
47
+ "message": f"Error al enviar mensaje. Status: {status_code}",
48
+ "response": response
49
+ }
50
+
51
+ except Exception as e:
52
+ logging.exception(f"Error inesperado al enviar mensaje a Google Chat: {e}")
53
+ return {
54
+ "success": False,
55
+ "message": f"Error interno del servidor: {str(e)}",
56
+ "response": None
57
+ }