iatoolkit 0.8.1__py3-none-any.whl → 0.63.4__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.
Potentially problematic release.
This version of iatoolkit might be problematic. Click here for more details.
- iatoolkit/__init__.py +8 -34
- iatoolkit/base_company.py +14 -3
- iatoolkit/common/routes.py +83 -52
- iatoolkit/common/session_manager.py +0 -1
- iatoolkit/common/util.py +0 -27
- iatoolkit/iatoolkit.py +61 -46
- iatoolkit/infra/llm_client.py +7 -8
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/repositories/database_manager.py +17 -2
- iatoolkit/repositories/models.py +31 -6
- iatoolkit/repositories/profile_repo.py +7 -2
- iatoolkit/services/auth_service.py +188 -0
- iatoolkit/services/branding_service.py +147 -0
- iatoolkit/services/dispatcher_service.py +10 -40
- iatoolkit/services/excel_service.py +15 -15
- iatoolkit/services/history_service.py +3 -12
- iatoolkit/services/jwt_service.py +15 -24
- iatoolkit/services/onboarding_service.py +43 -0
- iatoolkit/services/profile_service.py +97 -44
- iatoolkit/services/query_service.py +124 -81
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +67 -31
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
- iatoolkit/static/js/chat_history_button.js +126 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +130 -220
- 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 +52 -0
- iatoolkit/static/styles/chat_iatoolkit.css +329 -507
- iatoolkit/static/styles/chat_modal.css +95 -56
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +3 -12
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +40 -0
- iatoolkit/templates/base.html +8 -3
- iatoolkit/templates/change_password.html +54 -37
- iatoolkit/templates/chat.html +149 -66
- iatoolkit/templates/chat_modals.html +47 -18
- iatoolkit/templates/error.html +41 -8
- iatoolkit/templates/forgot_password.html +37 -24
- iatoolkit/templates/index.html +140 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +105 -0
- iatoolkit/templates/signup.html +64 -66
- iatoolkit/views/base_login_view.py +81 -0
- iatoolkit/views/change_password_view.py +23 -12
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
- iatoolkit/views/forgot_password_view.py +23 -13
- iatoolkit/views/history_api_view.py +52 -0
- iatoolkit/views/home_view.py +58 -25
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +68 -0
- iatoolkit/views/llmquery_api_view.py +45 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +118 -34
- iatoolkit/views/logout_api_view.py +45 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
- iatoolkit/views/signup_view.py +38 -29
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
- iatoolkit/views/verify_user_view.py +13 -8
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
- iatoolkit-0.63.4.dist-info/RECORD +113 -0
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
- iatoolkit/common/auth.py +0 -200
- iatoolkit/static/images/arrow_up.png +0 -0
- iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
- iatoolkit/static/images/logo_clinica.png +0 -0
- iatoolkit/static/images/logo_iatoolkit.png +0 -0
- iatoolkit/static/images/logo_maxxa.png +0 -0
- iatoolkit/static/images/logo_notaria.png +0 -0
- iatoolkit/static/images/logo_tarjeta.png +0 -0
- iatoolkit/static/images/logo_umayor.png +0 -0
- iatoolkit/static/images/upload.png +0 -0
- iatoolkit/static/js/chat_history.js +0 -117
- iatoolkit/templates/home.html +0 -201
- iatoolkit/templates/login.html +0 -43
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -51
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -88
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit-0.8.1.dist-info/RECORD +0 -175
- tests/__init__.py +0 -5
- tests/common/__init__.py +0 -0
- tests/common/test_auth.py +0 -279
- tests/common/test_routes.py +0 -42
- tests/common/test_session_manager.py +0 -59
- tests/common/test_util.py +0 -444
- tests/companies/__init__.py +0 -5
- tests/conftest.py +0 -36
- tests/infra/__init__.py +0 -5
- tests/infra/connectors/__init__.py +0 -5
- tests/infra/connectors/test_google_drive_connector.py +0 -107
- tests/infra/connectors/test_local_file_connector.py +0 -85
- tests/infra/connectors/test_s3_connector.py +0 -95
- tests/infra/test_call_service.py +0 -92
- tests/infra/test_database_manager.py +0 -59
- tests/infra/test_gemini_adapter.py +0 -137
- tests/infra/test_google_chat_app.py +0 -68
- tests/infra/test_llm_client.py +0 -165
- tests/infra/test_llm_proxy.py +0 -122
- tests/infra/test_mail_app.py +0 -94
- tests/infra/test_openai_adapter.py +0 -105
- tests/infra/test_redis_session_manager_service.py +0 -117
- tests/repositories/__init__.py +0 -5
- tests/repositories/test_database_manager.py +0 -87
- tests/repositories/test_document_repo.py +0 -76
- tests/repositories/test_llm_query_repo.py +0 -340
- tests/repositories/test_models.py +0 -38
- tests/repositories/test_profile_repo.py +0 -142
- tests/repositories/test_tasks_repo.py +0 -76
- tests/repositories/test_vs_repo.py +0 -107
- tests/services/__init__.py +0 -5
- tests/services/test_dispatcher_service.py +0 -274
- tests/services/test_document_service.py +0 -181
- tests/services/test_excel_service.py +0 -208
- tests/services/test_file_processor_service.py +0 -121
- tests/services/test_history_service.py +0 -164
- tests/services/test_jwt_service.py +0 -255
- tests/services/test_load_documents_service.py +0 -112
- tests/services/test_mail_service.py +0 -70
- tests/services/test_profile_service.py +0 -379
- tests/services/test_prompt_manager_service.py +0 -190
- tests/services/test_query_service.py +0 -243
- tests/services/test_search_service.py +0 -39
- tests/services/test_sql_service.py +0 -160
- tests/services/test_tasks_service.py +0 -252
- tests/services/test_user_feedback_service.py +0 -389
- tests/services/test_user_session_context_service.py +0 -132
- tests/views/__init__.py +0 -5
- tests/views/test_change_password_view.py +0 -191
- tests/views/test_chat_token_request_view.py +0 -188
- tests/views/test_chat_view.py +0 -98
- tests/views/test_download_file_view.py +0 -149
- tests/views/test_external_chat_login_view.py +0 -120
- tests/views/test_external_login_view.py +0 -102
- tests/views/test_file_store_view.py +0 -128
- tests/views/test_forgot_password_view.py +0 -142
- tests/views/test_history_view.py +0 -336
- tests/views/test_home_view.py +0 -61
- tests/views/test_llm_query_view.py +0 -154
- tests/views/test_login_view.py +0 -114
- tests/views/test_prompt_view.py +0 -111
- tests/views/test_signup_view.py +0 -140
- tests/views/test_tasks_review_view.py +0 -104
- tests/views/test_tasks_view.py +0 -130
- tests/views/test_user_feedback_view.py +0 -214
- tests/views/test_verify_user_view.py +0 -110
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
tests/infra/test_llm_client.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# tests/test_llm_client.py
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from unittest.mock import patch, MagicMock
|
|
5
|
-
from iatoolkit.infra.llm_client import llmClient
|
|
6
|
-
from iatoolkit.infra.llm_response import LLMResponse, ToolCall, Usage
|
|
7
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
8
|
-
from iatoolkit.repositories.models import Company
|
|
9
|
-
import json
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestLLMClient:
|
|
13
|
-
def setup_method(self):
|
|
14
|
-
"""Setup común para todos los tests"""
|
|
15
|
-
# Mocks de dependencias inyectadas
|
|
16
|
-
self.dispatcher_mock = MagicMock()
|
|
17
|
-
self.llmquery_repo = MagicMock()
|
|
18
|
-
self.util_mock = MagicMock()
|
|
19
|
-
self.llm_proxy_factory = MagicMock()
|
|
20
|
-
self.injector_mock = MagicMock()
|
|
21
|
-
|
|
22
|
-
# Mock del LLMProxy que será devuelto por la fábrica
|
|
23
|
-
self.mock_proxy = MagicMock()
|
|
24
|
-
self.llm_proxy_factory.create_for_company.return_value = self.mock_proxy
|
|
25
|
-
|
|
26
|
-
# Mock company
|
|
27
|
-
self.company = Company(id=1, name='Test Company', short_name='test_company')
|
|
28
|
-
|
|
29
|
-
# Mock de variables de entorno
|
|
30
|
-
self.env_patcher = patch.dict('os.environ', {'LLM_MODEL': 'gpt-4o'})
|
|
31
|
-
self.env_patcher.start()
|
|
32
|
-
|
|
33
|
-
# Mock tiktoken
|
|
34
|
-
self.tiktoken_patcher = patch('iatoolkit.infra.llm_client.tiktoken')
|
|
35
|
-
self.mock_tiktoken = self.tiktoken_patcher.start()
|
|
36
|
-
self.mock_tiktoken.encoding_for_model.return_value = MagicMock()
|
|
37
|
-
|
|
38
|
-
# Instance of the client under test
|
|
39
|
-
self.client = llmClient(
|
|
40
|
-
llmquery_repo=self.llmquery_repo,
|
|
41
|
-
util=self.util_mock,
|
|
42
|
-
llm_proxy_factory=self.llm_proxy_factory
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
# Respuesta mock estándar del LLM
|
|
46
|
-
self.mock_llm_response = LLMResponse(
|
|
47
|
-
id='response_123', model='gpt-4o', status='completed',
|
|
48
|
-
output_text=json.dumps({"answer": "Test response", "aditional_data": {}}),
|
|
49
|
-
output=[], usage=Usage(input_tokens=100, output_tokens=50, total_tokens=150)
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
def teardown_method(self):
|
|
53
|
-
"""Limpieza después de cada test"""
|
|
54
|
-
patch.stopall()
|
|
55
|
-
|
|
56
|
-
def test_init_missing_llm_model(self):
|
|
57
|
-
"""Test que la inicialización falla si falta LLM_MODEL."""
|
|
58
|
-
with patch.dict('os.environ', {}, clear=True):
|
|
59
|
-
with pytest.raises(IAToolkitException, match="LLM_MODEL"):
|
|
60
|
-
llmClient(self.llmquery_repo, self.util_mock, self.llm_proxy_factory)
|
|
61
|
-
|
|
62
|
-
def test_invoke_success(self):
|
|
63
|
-
"""Test de una llamada invoke exitosa."""
|
|
64
|
-
self.mock_proxy.create_response.return_value = self.mock_llm_response
|
|
65
|
-
|
|
66
|
-
result = self.client.invoke(
|
|
67
|
-
company=self.company, user_identifier='user1', previous_response_id='prev1',
|
|
68
|
-
question='q', context='c', tools=[], text={}
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
self.llm_proxy_factory.create_for_company.assert_called_once_with(self.company)
|
|
72
|
-
self.mock_proxy.create_response.assert_called_once()
|
|
73
|
-
assert result['valid_response'] is True
|
|
74
|
-
assert 'Test response' in result['answer']
|
|
75
|
-
assert result['response_id'] == 'response_123'
|
|
76
|
-
self.llmquery_repo.add_query.assert_called_once()
|
|
77
|
-
|
|
78
|
-
def test_invoke_handles_function_calls(self):
|
|
79
|
-
"""Tests that invoke correctly handles function calls."""
|
|
80
|
-
# 1. Create a mock for the dispatcher service
|
|
81
|
-
dispatcher_mock = MagicMock()
|
|
82
|
-
dispatcher_mock.dispatch.return_value = '{"status": "ok"}'
|
|
83
|
-
|
|
84
|
-
# 2. Create a mock injector that knows how to provide the dispatcher mock
|
|
85
|
-
injector_mock = MagicMock()
|
|
86
|
-
injector_mock.get.return_value = dispatcher_mock
|
|
87
|
-
|
|
88
|
-
# 3. Create a mock IAToolkit instance
|
|
89
|
-
toolkit_mock = MagicMock()
|
|
90
|
-
# Make its _get_injector() method return our mock injector
|
|
91
|
-
toolkit_mock.get_injector.return_value = injector_mock
|
|
92
|
-
|
|
93
|
-
# 4. Use patch to replace `current_iatoolkit` with our mock toolkit
|
|
94
|
-
with patch('iatoolkit.current_iatoolkit', return_value=toolkit_mock):
|
|
95
|
-
# 5. Define the sequence of LLM responses
|
|
96
|
-
tool_call = ToolCall('call1', 'function_call', 'test_func', '{"a": 1}')
|
|
97
|
-
response_with_tools = LLMResponse('r1', 'gpt-4o', 'completed', '', [tool_call], Usage(10, 5, 15))
|
|
98
|
-
self.mock_proxy.create_response.side_effect = [response_with_tools, self.mock_llm_response]
|
|
99
|
-
|
|
100
|
-
# 6. Invoke the client. Now, when it calls current_iatoolkit, it will get our mock.
|
|
101
|
-
self.client.invoke(
|
|
102
|
-
company=self.company, user_identifier='user1', previous_response_id='prev1',
|
|
103
|
-
question='q', context='c', tools=[{}], text={}
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# 7. Assertions
|
|
107
|
-
assert self.mock_proxy.create_response.call_count == 2
|
|
108
|
-
|
|
109
|
-
# Verify that the dispatcher was correctly retrieved and called
|
|
110
|
-
dispatcher_mock.dispatch.assert_called_once_with(
|
|
111
|
-
company_name='test_company', action='test_func', a=1
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
# Verify that the function output was reinjected into the history
|
|
115
|
-
second_call_args = self.mock_proxy.create_response.call_args_list[1].kwargs
|
|
116
|
-
function_output_message = second_call_args['input'][1]
|
|
117
|
-
assert function_output_message.get('type') == 'function_call_output'
|
|
118
|
-
assert function_output_message.get('output') == '{"status": "ok"}'
|
|
119
|
-
|
|
120
|
-
def test_invoke_llm_api_error_propagates(self):
|
|
121
|
-
"""Test que los errores de la API del LLM se propagan como IAToolkitException."""
|
|
122
|
-
self.mock_proxy.create_response.side_effect = Exception("API Communication Error")
|
|
123
|
-
|
|
124
|
-
with pytest.raises(IAToolkitException, match="Error calling LLM API"):
|
|
125
|
-
self.client.invoke(
|
|
126
|
-
company=self.company, user_identifier='user1', previous_response_id='prev1',
|
|
127
|
-
question='q', context='c', tools=[], text={}
|
|
128
|
-
)
|
|
129
|
-
# Verificar que se guarda un registro de error en la BD
|
|
130
|
-
self.llmquery_repo.add_query.assert_called_once()
|
|
131
|
-
log_arg = self.llmquery_repo.add_query.call_args[0][0]
|
|
132
|
-
assert log_arg.valid_response is False
|
|
133
|
-
assert "API Communication Error" in log_arg.output
|
|
134
|
-
|
|
135
|
-
def test_set_company_context_success(self):
|
|
136
|
-
"""Test de la configuración exitosa del contexto de la empresa."""
|
|
137
|
-
context_response = LLMResponse('ctx1', 'gpt-4o', 'completed', 'OK', [], Usage(10, 2, 12))
|
|
138
|
-
self.mock_proxy.create_response.return_value = context_response
|
|
139
|
-
|
|
140
|
-
response_id = self.client.set_company_context(
|
|
141
|
-
company=self.company, company_base_context="System prompt"
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
assert response_id == 'ctx1'
|
|
145
|
-
self.llm_proxy_factory.create_for_company.assert_called_once_with(self.company)
|
|
146
|
-
self.mock_proxy.create_response.assert_called_once()
|
|
147
|
-
call_args = self.mock_proxy.create_response.call_args.kwargs['input'][0]
|
|
148
|
-
assert call_args['role'] == 'system'
|
|
149
|
-
assert call_args['content'] == 'System prompt'
|
|
150
|
-
|
|
151
|
-
def test_decode_response_valid_json(self):
|
|
152
|
-
"""Test de decodificación de una respuesta JSON válida."""
|
|
153
|
-
response = LLMResponse('r1', 'm1', 'completed', '```json\n{"answer": "hola"}\n```', [], Usage(1, 1, 2))
|
|
154
|
-
|
|
155
|
-
# Simular una respuesta con fallback
|
|
156
|
-
with patch('json.loads', return_value={'answer': 'hola'}):
|
|
157
|
-
decoded = self.client.decode_response(response)
|
|
158
|
-
assert decoded['answer_format'] == 'json_fallback'
|
|
159
|
-
|
|
160
|
-
# Simular una respuesta completa y válida
|
|
161
|
-
with patch('json.loads', return_value={'answer': 'hola', 'aditional_data': {}}):
|
|
162
|
-
decoded = self.client.decode_response(response)
|
|
163
|
-
assert decoded['status'] is True
|
|
164
|
-
assert decoded['answer'] == 'hola'
|
|
165
|
-
assert decoded['answer_format'] == 'json_string'
|
tests/infra/test_llm_proxy.py
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import patch, MagicMock
|
|
3
|
-
from iatoolkit.infra.llm_proxy import LLMProxy
|
|
4
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestLLMProxy:
|
|
8
|
-
|
|
9
|
-
def setup_method(self):
|
|
10
|
-
"""Configuración común para las pruebas de LLMProxy."""
|
|
11
|
-
self.util_mock = MagicMock()
|
|
12
|
-
self.util_mock.decrypt_key.side_effect = lambda x: f"decrypted_{x}"
|
|
13
|
-
|
|
14
|
-
# Mocks para los clientes de los proveedores y sus adaptadores
|
|
15
|
-
self.openai_patcher = patch('iatoolkit.infra.llm_proxy.OpenAI')
|
|
16
|
-
self.gemini_patcher = patch('iatoolkit.infra.llm_proxy.genai')
|
|
17
|
-
self.openai_adapter_patcher = patch('iatoolkit.infra.llm_proxy.OpenAIAdapter')
|
|
18
|
-
self.gemini_adapter_patcher = patch('iatoolkit.infra.llm_proxy.GeminiAdapter')
|
|
19
|
-
|
|
20
|
-
self.mock_openai_class = self.openai_patcher.start()
|
|
21
|
-
self.mock_gemini_module = self.gemini_patcher.start()
|
|
22
|
-
self.mock_openai_adapter_class = self.openai_adapter_patcher.start()
|
|
23
|
-
self.mock_gemini_adapter_class = self.gemini_adapter_patcher.start()
|
|
24
|
-
|
|
25
|
-
self.mock_openai_adapter_instance = MagicMock()
|
|
26
|
-
self.mock_gemini_adapter_instance = MagicMock()
|
|
27
|
-
self.mock_openai_adapter_class.return_value = self.mock_openai_adapter_instance
|
|
28
|
-
self.mock_gemini_adapter_class.return_value = self.mock_gemini_adapter_instance
|
|
29
|
-
|
|
30
|
-
# --- Mocks de Compañías usando MagicMock para evitar TypeError ---
|
|
31
|
-
|
|
32
|
-
# Compañía con ambas claves
|
|
33
|
-
self.company_both = MagicMock()
|
|
34
|
-
self.company_both.short_name = 'comp_both'
|
|
35
|
-
self.company_both.name = 'Both'
|
|
36
|
-
self.company_both.openai_api_key = 'oa_key'
|
|
37
|
-
self.company_both.gemini_api_key = 'g_key'
|
|
38
|
-
|
|
39
|
-
# Compañía solo con OpenAI (la clave de gemini es None)
|
|
40
|
-
self.company_openai = MagicMock()
|
|
41
|
-
self.company_openai.short_name = 'comp_oa'
|
|
42
|
-
self.company_openai.name = 'OpenAI'
|
|
43
|
-
self.company_openai.openai_api_key = 'oa_key'
|
|
44
|
-
|
|
45
|
-
# Compañía solo con Gemini (la clave de openai es None)
|
|
46
|
-
self.company_gemini = MagicMock()
|
|
47
|
-
self.company_gemini.short_name = 'comp_g'
|
|
48
|
-
self.company_gemini.name = 'Gemini'
|
|
49
|
-
|
|
50
|
-
# Compañía sin claves
|
|
51
|
-
self.company_none = MagicMock()
|
|
52
|
-
self.company_none.short_name = 'comp_none'
|
|
53
|
-
self.company_none.name = 'None'
|
|
54
|
-
self.company_none.openai_api_key = None
|
|
55
|
-
|
|
56
|
-
patch.dict('os.environ', {'OPENAI_API_KEY': 'fb_oa', 'GEMINI_API_KEY': 'fb_g'}).start()
|
|
57
|
-
|
|
58
|
-
# Instancia "fábrica" bajo prueba
|
|
59
|
-
self.proxy_factory = LLMProxy(util=self.util_mock)
|
|
60
|
-
|
|
61
|
-
def teardown_method(self):
|
|
62
|
-
patch.stopall()
|
|
63
|
-
LLMProxy._clients_cache.clear()
|
|
64
|
-
|
|
65
|
-
def test_create_for_company(self):
|
|
66
|
-
"""Prueba que el factory method crea un Proxy con ambos clientes."""
|
|
67
|
-
proxy = self.proxy_factory.create_for_company(self.company_openai)
|
|
68
|
-
|
|
69
|
-
self.mock_openai_class.assert_called_once_with(api_key='decrypted_oa_key')
|
|
70
|
-
|
|
71
|
-
assert isinstance(proxy, LLMProxy)
|
|
72
|
-
self.mock_openai_adapter_class.assert_called_once()
|
|
73
|
-
self.mock_gemini_adapter_class.assert_called_once()
|
|
74
|
-
assert proxy.openai_adapter is not None
|
|
75
|
-
assert proxy.gemini_adapter is not None
|
|
76
|
-
|
|
77
|
-
def test_create_for_company_raises_error_if_no_keys(self):
|
|
78
|
-
"""Prueba que el factory method lanza una excepción si no hay ninguna clave disponible."""
|
|
79
|
-
with patch.dict('os.environ', {}, clear=True):
|
|
80
|
-
# Usar una compañía que realmente no tenga los atributos
|
|
81
|
-
company_truly_none = MagicMock()
|
|
82
|
-
company_truly_none.openai_api_key = None
|
|
83
|
-
company_truly_none.gemini_api_key = None
|
|
84
|
-
|
|
85
|
-
with pytest.raises(IAToolkitException, match="no tiene configuradas API keys"):
|
|
86
|
-
self.proxy_factory.create_for_company(company_truly_none)
|
|
87
|
-
|
|
88
|
-
def test_client_caching_works(self):
|
|
89
|
-
"""Prueba que los clientes se cachean y reutilizan entre llamadas."""
|
|
90
|
-
self.proxy_factory.create_for_company(self.company_openai)
|
|
91
|
-
self.proxy_factory.create_for_company(self.company_openai)
|
|
92
|
-
self.mock_openai_class.assert_called_once_with(api_key='decrypted_oa_key')
|
|
93
|
-
|
|
94
|
-
def test_routing_to_openai_adapter(self):
|
|
95
|
-
"""Prueba el enrutamiento correcto hacia el adaptador de OpenAI."""
|
|
96
|
-
self.util_mock.is_openai_model.return_value = True
|
|
97
|
-
self.util_mock.is_gemini_model.return_value = False
|
|
98
|
-
|
|
99
|
-
proxy = self.proxy_factory.create_for_company(self.company_openai)
|
|
100
|
-
|
|
101
|
-
# El método create_response no está definido en el MagicMock `proxy`,
|
|
102
|
-
# así que llamamos al método real en la instancia de fábrica para obtener una instancia real
|
|
103
|
-
work_proxy = self.proxy_factory.create_for_company(self.company_openai)
|
|
104
|
-
work_proxy.create_response(model='gpt-4', input=[])
|
|
105
|
-
|
|
106
|
-
self.mock_openai_adapter_instance.create_response.assert_called_once()
|
|
107
|
-
self.mock_gemini_adapter_instance.create_response.assert_not_called()
|
|
108
|
-
|
|
109
|
-
def test_routing_to_gemini_adapter(self):
|
|
110
|
-
"""Prueba el enrutamiento correcto hacia el adaptador de OpenAI."""
|
|
111
|
-
self.util_mock.is_openai_model.return_value = False
|
|
112
|
-
self.util_mock.is_gemini_model.return_value = True
|
|
113
|
-
|
|
114
|
-
proxy = self.proxy_factory.create_for_company(self.company_openai)
|
|
115
|
-
|
|
116
|
-
# El método create_response no está definido en el MagicMock `proxy`,
|
|
117
|
-
# así que llamamos al método real en la instancia de fábrica para obtener una instancia real
|
|
118
|
-
work_proxy = self.proxy_factory.create_for_company(self.company_gemini)
|
|
119
|
-
work_proxy.create_response(model='gemini', input=[])
|
|
120
|
-
|
|
121
|
-
self.mock_gemini_adapter_instance.create_response.assert_called_once()
|
|
122
|
-
self.mock_openai_adapter_instance.create_response.assert_not_called()
|
tests/infra/test_mail_app.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Product: IAToolkit
|
|
3
|
-
#
|
|
4
|
-
# IAToolkit is open source software.
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
from unittest.mock import MagicMock, patch
|
|
8
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
9
|
-
from iatoolkit.infra.mail_app import MailApp
|
|
10
|
-
import os
|
|
11
|
-
|
|
12
|
-
class TestMailApp:
|
|
13
|
-
@classmethod
|
|
14
|
-
def setup_class(cls):
|
|
15
|
-
cls.patcher = patch.dict(os.environ, {"BREVO_API_KEY": "my-api-key"})
|
|
16
|
-
cls.patcher.start()
|
|
17
|
-
|
|
18
|
-
def setup_method(self):
|
|
19
|
-
# Crear una instancia de MailApp simulada
|
|
20
|
-
self.mail_app = MailApp()
|
|
21
|
-
|
|
22
|
-
# Mock de la API de correos transaccionales
|
|
23
|
-
self.mock_mail_api = MagicMock()
|
|
24
|
-
self.mail_app.mail_api = self.mock_mail_api
|
|
25
|
-
|
|
26
|
-
def teardown_method(self):
|
|
27
|
-
self.patcher.stop()
|
|
28
|
-
|
|
29
|
-
@patch("iatoolkit.infra.mail_app.sib_api_v3_sdk.SendSmtpEmail")
|
|
30
|
-
def test_send_email_success(self, mock_send_smtp_email):
|
|
31
|
-
"""Prueba el envío exitoso del correo electrónico."""
|
|
32
|
-
# Simular una respuesta exitosa de la API
|
|
33
|
-
mock_response = MagicMock()
|
|
34
|
-
mock_response.message_id = "100"
|
|
35
|
-
self.mock_mail_api.send_transac_email.return_value = mock_response
|
|
36
|
-
|
|
37
|
-
# Llamar al método `send_email`
|
|
38
|
-
response = self.mail_app.send_email(
|
|
39
|
-
to="test@domain.com",
|
|
40
|
-
subject="Test Subject",
|
|
41
|
-
body="<p>This is a test email</p>"
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
# Verificaciones
|
|
45
|
-
self.mock_mail_api.send_transac_email.assert_called_once()
|
|
46
|
-
assert response == mock_response
|
|
47
|
-
|
|
48
|
-
@patch("iatoolkit.infra.mail_app.sib_api_v3_sdk.SendSmtpEmail")
|
|
49
|
-
def test_send_email_when_api_error(self, mock_send_smtp_email):
|
|
50
|
-
"""Prueba el envío exitoso del correo electrónico."""
|
|
51
|
-
# Simular una respuesta exitosa de la API
|
|
52
|
-
mock_response = MagicMock()
|
|
53
|
-
self.mock_mail_api.send_transac_email.return_value = mock_response
|
|
54
|
-
|
|
55
|
-
with pytest.raises(IAToolkitException) as excinfo:
|
|
56
|
-
response = self.mail_app.send_email(
|
|
57
|
-
to="test@domain.com",
|
|
58
|
-
subject="Test Subject",
|
|
59
|
-
body="<p>This is a test email</p>"
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# Verificar que la excepción es la esperada
|
|
63
|
-
assert excinfo.value.error_type == IAToolkitException.ErrorType.MAIL_ERROR
|
|
64
|
-
assert "Brevo no retornó message_id" in str(excinfo.value)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@patch("iatoolkit.infra.mail_app.sib_api_v3_sdk.SendSmtpEmail")
|
|
68
|
-
def test_send_email_with_error(self, mock_send_smtp_email):
|
|
69
|
-
"""Prueba el manejo de errores cuando se lanza una excepción."""
|
|
70
|
-
# Simular que la API lanza una excepción
|
|
71
|
-
self.mock_mail_api.send_transac_email.side_effect = Exception("API error")
|
|
72
|
-
|
|
73
|
-
with pytest.raises(IAToolkitException) as excinfo:
|
|
74
|
-
self.mail_app.send_email(
|
|
75
|
-
to="test@domain.com",
|
|
76
|
-
subject="Test Subject",
|
|
77
|
-
body="<p>This is a test email</p>"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
# Verificar que la excepción es la esperada
|
|
81
|
-
assert excinfo.value.error_type == IAToolkitException.ErrorType.MAIL_ERROR
|
|
82
|
-
assert "No se pudo enviar correo: API error" in str(excinfo.value)
|
|
83
|
-
|
|
84
|
-
def test_initial_configuration(self):
|
|
85
|
-
"""Prueba la configuración inicial de MailApp."""
|
|
86
|
-
with patch("os.getenv", return_value="mocked_api_key") as mock_getenv:
|
|
87
|
-
mail_app = MailApp()
|
|
88
|
-
|
|
89
|
-
# Verificar que la configuración fue inicializada correctamente
|
|
90
|
-
assert mail_app.configuration.api_key['api-key'] == "mocked_api_key"
|
|
91
|
-
assert mail_app.sender == {'email': 'ia@iatoolkit.com', 'name': 'IA Toolkit'}
|
|
92
|
-
|
|
93
|
-
# Asegurar que os.getenv fue llamado para recuperar la clave de API
|
|
94
|
-
mock_getenv.assert_called_once_with("BREVO_API_KEY")
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import Mock, patch
|
|
3
|
-
|
|
4
|
-
from iatoolkit.infra.llm_proxy import LLMProxy
|
|
5
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
6
|
-
from iatoolkit.infra.llm_response import LLMResponse, Usage
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestLLMProxy:
|
|
10
|
-
"""Tests para la clase LLMProxy."""
|
|
11
|
-
|
|
12
|
-
def setup_method(self):
|
|
13
|
-
"""Configura el entorno de prueba para cada método."""
|
|
14
|
-
# Parcheamos las clases de los adaptadores en el módulo donde se utilizan (llm_proxy)
|
|
15
|
-
self.openai_patcher = patch('iatoolkit.infra.llm_proxy.OpenAIAdapter', autospec=True)
|
|
16
|
-
self.gemini_patcher = patch('iatoolkit.infra.llm_proxy.GeminiAdapter', autospec=True)
|
|
17
|
-
|
|
18
|
-
# Iniciamos los patchers
|
|
19
|
-
self.MockOpenAIAdapter = self.openai_patcher.start()
|
|
20
|
-
self.MockGeminiAdapter = self.gemini_patcher.start()
|
|
21
|
-
|
|
22
|
-
# Mocks para los clientes de las APIs
|
|
23
|
-
self.mock_openai_client = Mock()
|
|
24
|
-
self.mock_gemini_client = Mock()
|
|
25
|
-
|
|
26
|
-
# Instanciamos LLMProxy. Esto usará las clases de adaptadores mockeadas.
|
|
27
|
-
self.util_mock = Mock()
|
|
28
|
-
self.proxy = LLMProxy(
|
|
29
|
-
openai_client=self.mock_openai_client,
|
|
30
|
-
gemini_client=self.mock_gemini_client,
|
|
31
|
-
util=self.util_mock
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
# Obtenemos una referencia a las instancias mockeadas que LLMProxy crea internamente
|
|
35
|
-
self.mock_openai_adapter_instance = self.proxy.openai_adapter
|
|
36
|
-
self.mock_gemini_adapter_instance = self.proxy.gemini_adapter
|
|
37
|
-
|
|
38
|
-
def teardown_method(self):
|
|
39
|
-
"""Limpia el entorno de prueba después de cada método."""
|
|
40
|
-
self.openai_patcher.stop()
|
|
41
|
-
self.gemini_patcher.stop()
|
|
42
|
-
|
|
43
|
-
def test_create_response_routes_to_openai(self):
|
|
44
|
-
"""
|
|
45
|
-
Prueba que create_response enruta correctamente al adaptador de OpenAI
|
|
46
|
-
para un modelo de OpenAI.
|
|
47
|
-
"""
|
|
48
|
-
model = "gpt-4"
|
|
49
|
-
input_data = [{"role": "user", "content": "Hola"}]
|
|
50
|
-
mock_usage = Usage(input_tokens=10, output_tokens=10, total_tokens=20)
|
|
51
|
-
mock_response = LLMResponse(id="resp-123", model=model, status="completed", output_text="Hola!", output=[], usage=mock_usage)
|
|
52
|
-
self.mock_openai_adapter_instance.create_response.return_value = mock_response
|
|
53
|
-
self.util_mock.is_openai_model.return_value = True
|
|
54
|
-
response = self.proxy.create_response(model=model, input=input_data)
|
|
55
|
-
|
|
56
|
-
self.mock_openai_adapter_instance.create_response.assert_called_once_with(
|
|
57
|
-
model=model,
|
|
58
|
-
input=input_data
|
|
59
|
-
)
|
|
60
|
-
self.mock_gemini_adapter_instance.create_response.assert_not_called()
|
|
61
|
-
assert response == mock_response
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_create_response_with_all_params_openai(self):
|
|
65
|
-
"""
|
|
66
|
-
Prueba el enrutamiento a OpenAI con todos los parámetros opcionales.
|
|
67
|
-
"""
|
|
68
|
-
model = "gpt-4o"
|
|
69
|
-
input_data = [{"role": "user", "content": "Hola"}]
|
|
70
|
-
tools_data = [{"type": "function", "function": {"name": "get_weather"}}]
|
|
71
|
-
self.util_mock.is_openai_model.return_value = True
|
|
72
|
-
|
|
73
|
-
self.proxy.create_response(
|
|
74
|
-
model=model,
|
|
75
|
-
input=input_data,
|
|
76
|
-
previous_response_id="prev-123",
|
|
77
|
-
tools=tools_data,
|
|
78
|
-
text={"some": "text"},
|
|
79
|
-
reasoning={"some": "reasoning"},
|
|
80
|
-
tool_choice="specific_tool"
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
self.mock_openai_adapter_instance.create_response.assert_called_once_with(
|
|
84
|
-
model=model,
|
|
85
|
-
input=input_data,
|
|
86
|
-
previous_response_id="prev-123",
|
|
87
|
-
tools=tools_data,
|
|
88
|
-
text={"some": "text"},
|
|
89
|
-
reasoning={"some": "reasoning"},
|
|
90
|
-
tool_choice="specific_tool"
|
|
91
|
-
)
|
|
92
|
-
self.mock_gemini_adapter_instance.create_response.assert_not_called()
|
|
93
|
-
|
|
94
|
-
def test_create_response_raises_for_unsupported_model(self):
|
|
95
|
-
"""
|
|
96
|
-
Prueba que create_response lanza una IAToolkitException para un modelo no soportado.
|
|
97
|
-
"""
|
|
98
|
-
self.util_mock.is_openai_model.return_value = False
|
|
99
|
-
self.util_mock.is_gemini_model.return_value = False
|
|
100
|
-
with pytest.raises(IAToolkitException) as excinfo:
|
|
101
|
-
self.proxy.create_response(model="unsupported-model", input=[])
|
|
102
|
-
|
|
103
|
-
assert excinfo.value.error_type == IAToolkitException.ErrorType.LLM_ERROR
|
|
104
|
-
assert "Modelo no soportado: unsupported-model" in str(excinfo.value)
|
|
105
|
-
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
import json
|
|
3
|
-
from unittest.mock import patch, MagicMock
|
|
4
|
-
from iatoolkit.infra.redis_session_manager import RedisSessionManager
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestRedisSessionManager(unittest.TestCase):
|
|
8
|
-
|
|
9
|
-
def setUp(self):
|
|
10
|
-
"""
|
|
11
|
-
Configura los patches y mocks comunes para las pruebas.
|
|
12
|
-
La clave aquí es parchear _get_client para evitar cualquier llamada de red.
|
|
13
|
-
"""
|
|
14
|
-
# Parchear el método que obtiene el cliente Redis
|
|
15
|
-
self.get_client_patcher = patch('iatoolkit.infra.redis_session_manager.RedisSessionManager._get_client')
|
|
16
|
-
|
|
17
|
-
# Iniciar el parche y obtener el mock del método
|
|
18
|
-
self.mock_get_client = self.get_client_patcher.start()
|
|
19
|
-
|
|
20
|
-
# Crear un mock para simular el cliente Redis real (ej. redis.Redis())
|
|
21
|
-
self.mock_redis_client = MagicMock()
|
|
22
|
-
|
|
23
|
-
# Configurar el método parcheado para que siempre devuelva nuestro cliente mockeado
|
|
24
|
-
self.mock_get_client.return_value = self.mock_redis_client
|
|
25
|
-
|
|
26
|
-
def tearDown(self):
|
|
27
|
-
patch.stopall()
|
|
28
|
-
|
|
29
|
-
def test_set(self):
|
|
30
|
-
"""Prueba que el método set llama correctamente a `client.set`."""
|
|
31
|
-
RedisSessionManager.set('my_key', 'my_value', ex=3600)
|
|
32
|
-
|
|
33
|
-
# Verificar que se obtuvo el cliente
|
|
34
|
-
self.mock_get_client.assert_called_once()
|
|
35
|
-
|
|
36
|
-
# Verificar que se llamó al método `set` del cliente Redis con los argumentos correctos
|
|
37
|
-
self.mock_redis_client.set.assert_called_once_with('my_key', 'my_value', ex=3600)
|
|
38
|
-
|
|
39
|
-
def test_get_existing_key(self):
|
|
40
|
-
"""Prueba que el método get devuelve el valor correcto si la clave existe."""
|
|
41
|
-
# Simular que Redis devuelve un valor
|
|
42
|
-
self.mock_redis_client.get.return_value = 'retrieved_value'
|
|
43
|
-
|
|
44
|
-
result = RedisSessionManager.get('existing_key')
|
|
45
|
-
|
|
46
|
-
self.assertEqual(result, 'retrieved_value')
|
|
47
|
-
self.mock_redis_client.get.assert_called_once_with('existing_key')
|
|
48
|
-
|
|
49
|
-
def test_get_non_existing_key(self):
|
|
50
|
-
"""Prueba que get devuelve el valor por defecto (string vacío) si la clave no existe."""
|
|
51
|
-
# Simular que Redis no encuentra la clave
|
|
52
|
-
self.mock_redis_client.get.return_value = None
|
|
53
|
-
|
|
54
|
-
result = RedisSessionManager.get('non_existing_key')
|
|
55
|
-
|
|
56
|
-
# El valor por defecto en la firma del método es ""
|
|
57
|
-
self.assertEqual(result, "")
|
|
58
|
-
|
|
59
|
-
def test_remove(self):
|
|
60
|
-
"""Prueba que el método remove llama a `client.delete`."""
|
|
61
|
-
RedisSessionManager.remove('key_to_delete')
|
|
62
|
-
|
|
63
|
-
self.mock_get_client.assert_called_once()
|
|
64
|
-
self.mock_redis_client.delete.assert_called_once_with('key_to_delete')
|
|
65
|
-
|
|
66
|
-
def test_set_json(self):
|
|
67
|
-
"""Prueba que el método set_json serializa el diccionario y llama a `set`."""
|
|
68
|
-
test_data = {'user': 'test', 'permissions': [1, 2, 3]}
|
|
69
|
-
expected_json_string = json.dumps(test_data)
|
|
70
|
-
|
|
71
|
-
RedisSessionManager.set_json('json_key', test_data, ex=60)
|
|
72
|
-
|
|
73
|
-
# Verificar que se llamó a set con el string JSON correcto
|
|
74
|
-
self.mock_redis_client.set.assert_called_once_with('json_key', expected_json_string, ex=60)
|
|
75
|
-
|
|
76
|
-
def test_get_json_existing_key(self):
|
|
77
|
-
"""Prueba que get_json obtiene un string y lo deserializa correctamente."""
|
|
78
|
-
test_data = {'user': 'test', 'id': 123}
|
|
79
|
-
json_string = json.dumps(test_data)
|
|
80
|
-
self.mock_redis_client.get.return_value = json_string
|
|
81
|
-
|
|
82
|
-
result = RedisSessionManager.get_json('json_key')
|
|
83
|
-
|
|
84
|
-
self.assertEqual(result, test_data)
|
|
85
|
-
|
|
86
|
-
def test_get_json_non_existing_key(self):
|
|
87
|
-
"""Prueba que get_json devuelve el diccionario por defecto si la clave no existe."""
|
|
88
|
-
self.mock_redis_client.get.return_value = "" # Simula una clave no encontrada
|
|
89
|
-
|
|
90
|
-
# Probar con el default por defecto ({})
|
|
91
|
-
result_default = RedisSessionManager.get_json('non_existing_key')
|
|
92
|
-
self.assertEqual(result_default, {})
|
|
93
|
-
|
|
94
|
-
# Probar con un default personalizado
|
|
95
|
-
custom_default = {"default": True}
|
|
96
|
-
result_custom = RedisSessionManager.get_json('non_existing_key', default=custom_default)
|
|
97
|
-
self.assertEqual(result_custom, custom_default)
|
|
98
|
-
|
|
99
|
-
@patch('iatoolkit.infra.redis_session_manager.logging')
|
|
100
|
-
def test_get_json_invalid_json(self, mock_logging):
|
|
101
|
-
"""
|
|
102
|
-
Prueba que get_json maneja un string inválido, loguea una advertencia
|
|
103
|
-
y devuelve el valor por defecto.
|
|
104
|
-
"""
|
|
105
|
-
invalid_json_string = '{"key": "value",}' # JSON mal formado
|
|
106
|
-
self.mock_redis_client.get.return_value = invalid_json_string
|
|
107
|
-
|
|
108
|
-
result = RedisSessionManager.get_json('invalid_json_key')
|
|
109
|
-
|
|
110
|
-
self.assertEqual(result, {})
|
|
111
|
-
|
|
112
|
-
# Verificar que se registró una advertencia
|
|
113
|
-
mock_logging.warning.assert_called_once()
|
|
114
|
-
# Verificar que el mensaje de log contiene información útil
|
|
115
|
-
log_message = mock_logging.warning.call_args[0][0]
|
|
116
|
-
self.assertIn("Invalid JSON", log_message)
|
|
117
|
-
self.assertIn("invalid_json_key", log_message)
|