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.

Files changed (159) hide show
  1. iatoolkit/__init__.py +8 -34
  2. iatoolkit/base_company.py +14 -3
  3. iatoolkit/common/routes.py +83 -52
  4. iatoolkit/common/session_manager.py +0 -1
  5. iatoolkit/common/util.py +0 -27
  6. iatoolkit/iatoolkit.py +61 -46
  7. iatoolkit/infra/llm_client.py +7 -8
  8. iatoolkit/infra/openai_adapter.py +1 -1
  9. iatoolkit/infra/redis_session_manager.py +48 -2
  10. iatoolkit/repositories/database_manager.py +17 -2
  11. iatoolkit/repositories/models.py +31 -6
  12. iatoolkit/repositories/profile_repo.py +7 -2
  13. iatoolkit/services/auth_service.py +188 -0
  14. iatoolkit/services/branding_service.py +147 -0
  15. iatoolkit/services/dispatcher_service.py +10 -40
  16. iatoolkit/services/excel_service.py +15 -15
  17. iatoolkit/services/history_service.py +3 -12
  18. iatoolkit/services/jwt_service.py +15 -24
  19. iatoolkit/services/onboarding_service.py +43 -0
  20. iatoolkit/services/profile_service.py +97 -44
  21. iatoolkit/services/query_service.py +124 -81
  22. iatoolkit/services/tasks_service.py +1 -1
  23. iatoolkit/services/user_feedback_service.py +67 -31
  24. iatoolkit/services/user_session_context_service.py +112 -54
  25. iatoolkit/static/images/fernando.jpeg +0 -0
  26. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
  27. iatoolkit/static/js/chat_history_button.js +126 -0
  28. iatoolkit/static/js/chat_logout_button.js +36 -0
  29. iatoolkit/static/js/chat_main.js +130 -220
  30. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  31. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  32. iatoolkit/static/js/chat_reload_button.js +52 -0
  33. iatoolkit/static/styles/chat_iatoolkit.css +329 -507
  34. iatoolkit/static/styles/chat_modal.css +95 -56
  35. iatoolkit/static/styles/landing_page.css +182 -0
  36. iatoolkit/static/styles/onboarding.css +169 -0
  37. iatoolkit/system_prompts/query_main.prompt +3 -12
  38. iatoolkit/templates/_company_header.html +20 -0
  39. iatoolkit/templates/_login_widget.html +40 -0
  40. iatoolkit/templates/base.html +8 -3
  41. iatoolkit/templates/change_password.html +54 -37
  42. iatoolkit/templates/chat.html +149 -66
  43. iatoolkit/templates/chat_modals.html +47 -18
  44. iatoolkit/templates/error.html +41 -8
  45. iatoolkit/templates/forgot_password.html +37 -24
  46. iatoolkit/templates/index.html +140 -0
  47. iatoolkit/templates/login_simulation.html +34 -0
  48. iatoolkit/templates/onboarding_shell.html +105 -0
  49. iatoolkit/templates/signup.html +64 -66
  50. iatoolkit/views/base_login_view.py +81 -0
  51. iatoolkit/views/change_password_view.py +23 -12
  52. iatoolkit/views/external_login_view.py +61 -28
  53. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  54. iatoolkit/views/forgot_password_view.py +23 -13
  55. iatoolkit/views/history_api_view.py +52 -0
  56. iatoolkit/views/home_view.py +58 -25
  57. iatoolkit/views/index_view.py +14 -0
  58. iatoolkit/views/init_context_api_view.py +68 -0
  59. iatoolkit/views/llmquery_api_view.py +45 -0
  60. iatoolkit/views/login_simulation_view.py +81 -0
  61. iatoolkit/views/login_view.py +118 -34
  62. iatoolkit/views/logout_api_view.py +45 -0
  63. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
  64. iatoolkit/views/signup_view.py +38 -29
  65. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  66. iatoolkit/views/tasks_review_api_view.py +55 -0
  67. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
  68. iatoolkit/views/verify_user_view.py +13 -8
  69. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
  70. iatoolkit-0.63.4.dist-info/RECORD +113 -0
  71. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
  72. iatoolkit/common/auth.py +0 -200
  73. iatoolkit/static/images/arrow_up.png +0 -0
  74. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  75. iatoolkit/static/images/logo_clinica.png +0 -0
  76. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  77. iatoolkit/static/images/logo_maxxa.png +0 -0
  78. iatoolkit/static/images/logo_notaria.png +0 -0
  79. iatoolkit/static/images/logo_tarjeta.png +0 -0
  80. iatoolkit/static/images/logo_umayor.png +0 -0
  81. iatoolkit/static/images/upload.png +0 -0
  82. iatoolkit/static/js/chat_history.js +0 -117
  83. iatoolkit/templates/home.html +0 -201
  84. iatoolkit/templates/login.html +0 -43
  85. iatoolkit/views/chat_token_request_view.py +0 -98
  86. iatoolkit/views/chat_view.py +0 -51
  87. iatoolkit/views/download_file_view.py +0 -58
  88. iatoolkit/views/external_chat_login_view.py +0 -88
  89. iatoolkit/views/history_view.py +0 -57
  90. iatoolkit/views/llmquery_view.py +0 -65
  91. iatoolkit/views/tasks_review_view.py +0 -83
  92. iatoolkit-0.8.1.dist-info/RECORD +0 -175
  93. tests/__init__.py +0 -5
  94. tests/common/__init__.py +0 -0
  95. tests/common/test_auth.py +0 -279
  96. tests/common/test_routes.py +0 -42
  97. tests/common/test_session_manager.py +0 -59
  98. tests/common/test_util.py +0 -444
  99. tests/companies/__init__.py +0 -5
  100. tests/conftest.py +0 -36
  101. tests/infra/__init__.py +0 -5
  102. tests/infra/connectors/__init__.py +0 -5
  103. tests/infra/connectors/test_google_drive_connector.py +0 -107
  104. tests/infra/connectors/test_local_file_connector.py +0 -85
  105. tests/infra/connectors/test_s3_connector.py +0 -95
  106. tests/infra/test_call_service.py +0 -92
  107. tests/infra/test_database_manager.py +0 -59
  108. tests/infra/test_gemini_adapter.py +0 -137
  109. tests/infra/test_google_chat_app.py +0 -68
  110. tests/infra/test_llm_client.py +0 -165
  111. tests/infra/test_llm_proxy.py +0 -122
  112. tests/infra/test_mail_app.py +0 -94
  113. tests/infra/test_openai_adapter.py +0 -105
  114. tests/infra/test_redis_session_manager_service.py +0 -117
  115. tests/repositories/__init__.py +0 -5
  116. tests/repositories/test_database_manager.py +0 -87
  117. tests/repositories/test_document_repo.py +0 -76
  118. tests/repositories/test_llm_query_repo.py +0 -340
  119. tests/repositories/test_models.py +0 -38
  120. tests/repositories/test_profile_repo.py +0 -142
  121. tests/repositories/test_tasks_repo.py +0 -76
  122. tests/repositories/test_vs_repo.py +0 -107
  123. tests/services/__init__.py +0 -5
  124. tests/services/test_dispatcher_service.py +0 -274
  125. tests/services/test_document_service.py +0 -181
  126. tests/services/test_excel_service.py +0 -208
  127. tests/services/test_file_processor_service.py +0 -121
  128. tests/services/test_history_service.py +0 -164
  129. tests/services/test_jwt_service.py +0 -255
  130. tests/services/test_load_documents_service.py +0 -112
  131. tests/services/test_mail_service.py +0 -70
  132. tests/services/test_profile_service.py +0 -379
  133. tests/services/test_prompt_manager_service.py +0 -190
  134. tests/services/test_query_service.py +0 -243
  135. tests/services/test_search_service.py +0 -39
  136. tests/services/test_sql_service.py +0 -160
  137. tests/services/test_tasks_service.py +0 -252
  138. tests/services/test_user_feedback_service.py +0 -389
  139. tests/services/test_user_session_context_service.py +0 -132
  140. tests/views/__init__.py +0 -5
  141. tests/views/test_change_password_view.py +0 -191
  142. tests/views/test_chat_token_request_view.py +0 -188
  143. tests/views/test_chat_view.py +0 -98
  144. tests/views/test_download_file_view.py +0 -149
  145. tests/views/test_external_chat_login_view.py +0 -120
  146. tests/views/test_external_login_view.py +0 -102
  147. tests/views/test_file_store_view.py +0 -128
  148. tests/views/test_forgot_password_view.py +0 -142
  149. tests/views/test_history_view.py +0 -336
  150. tests/views/test_home_view.py +0 -61
  151. tests/views/test_llm_query_view.py +0 -154
  152. tests/views/test_login_view.py +0 -114
  153. tests/views/test_prompt_view.py +0 -111
  154. tests/views/test_signup_view.py +0 -140
  155. tests/views/test_tasks_review_view.py +0 -104
  156. tests/views/test_tasks_view.py +0 -130
  157. tests/views/test_user_feedback_view.py +0 -214
  158. tests/views/test_verify_user_view.py +0 -110
  159. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
@@ -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'
@@ -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()
@@ -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)
@@ -1,5 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-