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.
- iatoolkit/__init__.py +13 -35
- iatoolkit/base_company.py +74 -8
- iatoolkit/cli_commands.py +15 -23
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +46 -0
- iatoolkit/common/routes.py +141 -0
- iatoolkit/common/session_manager.py +24 -0
- iatoolkit/common/util.py +348 -0
- iatoolkit/company_registry.py +7 -8
- iatoolkit/iatoolkit.py +169 -96
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/gemini_adapter.py +356 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_client.py +429 -0
- iatoolkit/infra/llm_proxy.py +139 -0
- iatoolkit/infra/llm_response.py +40 -0
- iatoolkit/infra/mail_app.py +145 -0
- iatoolkit/infra/openai_adapter.py +90 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +144 -0
- iatoolkit/locales/es.yaml +140 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +110 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/llm_query_repo.py +91 -0
- iatoolkit/repositories/models.py +336 -0
- iatoolkit/repositories/profile_repo.py +123 -0
- iatoolkit/repositories/tasks_repo.py +52 -0
- iatoolkit/repositories/vs_repo.py +139 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +6 -6
- iatoolkit/services/branding_service.py +149 -0
- {services → iatoolkit/services}/dispatcher_service.py +39 -99
- {services → iatoolkit/services}/document_service.py +5 -5
- {services → iatoolkit/services}/excel_service.py +27 -21
- {services → iatoolkit/services}/file_processor_service.py +5 -5
- iatoolkit/services/help_content_service.py +30 -0
- {services → iatoolkit/services}/history_service.py +8 -16
- iatoolkit/services/i18n_service.py +104 -0
- {services → iatoolkit/services}/jwt_service.py +18 -27
- iatoolkit/services/language_service.py +77 -0
- {services → iatoolkit/services}/load_documents_service.py +19 -14
- {services → iatoolkit/services}/mail_service.py +5 -5
- iatoolkit/services/onboarding_service.py +43 -0
- {services → iatoolkit/services}/profile_service.py +155 -89
- {services → iatoolkit/services}/prompt_manager_service.py +26 -11
- {services → iatoolkit/services}/query_service.py +142 -104
- {services → iatoolkit/services}/search_service.py +21 -5
- {services → iatoolkit/services}/sql_service.py +24 -6
- {services → iatoolkit/services}/tasks_service.py +10 -10
- iatoolkit/services/user_feedback_service.py +103 -0
- iatoolkit/services/user_session_context_service.py +143 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +112 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +364 -0
- iatoolkit/static/js/chat_onboarding_button.js +97 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +592 -0
- iatoolkit/static/styles/chat_modal.css +169 -0
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/llm_output.css +115 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +5 -15
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/about.html +13 -0
- iatoolkit/templates/base.html +65 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +287 -0
- iatoolkit/templates/chat_modals.html +181 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +50 -0
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +104 -0
- iatoolkit/templates/signup.html +76 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +92 -0
- iatoolkit/views/change_password_view.py +117 -0
- iatoolkit/views/external_login_view.py +73 -0
- iatoolkit/views/file_store_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +72 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +73 -0
- iatoolkit/views/llmquery_api_view.py +57 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +153 -0
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/signup_view.py +94 -0
- iatoolkit/views/tasks_api_view.py +72 -0
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +62 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
- iatoolkit-0.66.2.dist-info/RECORD +119 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.4.2.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {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
|
+
}
|