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
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Product: IAToolkit
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
-
import os
|
|
7
|
-
import base64
|
|
8
|
-
import json
|
|
9
|
-
|
|
10
|
-
from iatoolkit.services.query_service import QueryService
|
|
11
|
-
from iatoolkit.services.prompt_manager_service import PromptService
|
|
12
|
-
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
13
|
-
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
14
|
-
from iatoolkit.repositories.models import User, Company
|
|
15
|
-
from iatoolkit.common.util import Utility
|
|
16
|
-
from iatoolkit.infra.llm_client import llmClient
|
|
17
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TestQueryService:
|
|
21
|
-
"""
|
|
22
|
-
Test suite for the QueryService.
|
|
23
|
-
It uses a consistent setup to mock all dependencies and the global `current_iatoolkit` context.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
@pytest.fixture(autouse=True)
|
|
27
|
-
def setup(self):
|
|
28
|
-
"""
|
|
29
|
-
Set up a consistent, mocked environment for each test.
|
|
30
|
-
This includes mocking all dependencies and the global `current_iatoolkit` context.
|
|
31
|
-
"""
|
|
32
|
-
# Mocks for all injected dependencies
|
|
33
|
-
self.document_service = MagicMock()
|
|
34
|
-
self.llmquery_repo = MagicMock()
|
|
35
|
-
self.profile_repo = MagicMock(spec=ProfileRepo)
|
|
36
|
-
self.prompt_service = MagicMock(spec=PromptService)
|
|
37
|
-
self.utility = MagicMock(spec=Utility)
|
|
38
|
-
self.llm_client_mock = MagicMock(spec=llmClient)
|
|
39
|
-
self.dispatcher = MagicMock()
|
|
40
|
-
self.session_context = MagicMock(spec=UserSessionContextService)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# --- Common mock configurations ---
|
|
44
|
-
self.utility.resolve_user_identifier.side_effect = lambda external_user_id=None,local_user_id=0: (external_user_id, False) or (f"User_{local_user_id}", True)
|
|
45
|
-
|
|
46
|
-
self.user = User(id=1, email='test@user.com')
|
|
47
|
-
self.company = Company(id=100, name='Test Company', short_name='test_company')
|
|
48
|
-
self.profile_repo.get_company_by_short_name.return_value = self.company
|
|
49
|
-
|
|
50
|
-
# Default mock session data
|
|
51
|
-
self.session_context.get_last_response_id.return_value = 'prev_response_id'
|
|
52
|
-
self.session_context.get_user_session_data.return_value = {'user_role': 'leader', 'user_name': 'session_user'}
|
|
53
|
-
|
|
54
|
-
# Default mock LLM response
|
|
55
|
-
self.mock_llm_response = {"valid_response": True, "answer": "LLM test response",
|
|
56
|
-
"response_id": "new_llm_response_id"}
|
|
57
|
-
self.llm_client_mock.invoke.return_value = self.mock_llm_response
|
|
58
|
-
self.llm_client_mock.set_company_context.return_value = 'new_context_response_id'
|
|
59
|
-
|
|
60
|
-
# Default mock Dispatcher responses
|
|
61
|
-
self.dispatcher.get_user_info.return_value = {'user_role': 'leader', 'user_name': 'test_user'}
|
|
62
|
-
self.dispatcher.get_company_services.return_value = [{'name': 'service1'}]
|
|
63
|
-
self.dispatcher.get_company_context.return_value = "Specific company context."
|
|
64
|
-
|
|
65
|
-
# Default prompt rendering
|
|
66
|
-
self.prompt_service.get_system_prompt.return_value = "System prompt template: {{ user_role }}"
|
|
67
|
-
self.utility.render_prompt_from_string.return_value = "Rendered system prompt: leader"
|
|
68
|
-
|
|
69
|
-
# Default model type
|
|
70
|
-
self.utility.is_openai_model.return_value = True
|
|
71
|
-
self.utility.is_gemini_model.return_value = False
|
|
72
|
-
|
|
73
|
-
# Create the service instance under test. This now works because the context is patched.
|
|
74
|
-
with patch.dict(os.environ, {"LLM_MODEL": "gpt-test"}):
|
|
75
|
-
self.service = QueryService(
|
|
76
|
-
llm_client=self.llm_client_mock,
|
|
77
|
-
document_service=self.document_service,
|
|
78
|
-
document_repo=MagicMock(),
|
|
79
|
-
llmquery_repo=self.llmquery_repo,
|
|
80
|
-
profile_repo=self.profile_repo,
|
|
81
|
-
prompt_service=self.prompt_service,
|
|
82
|
-
util=self.utility,
|
|
83
|
-
dispatcher=self.dispatcher,
|
|
84
|
-
session_context=self.session_context
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# File content for file loading tests
|
|
88
|
-
self.document_content = b'document content'
|
|
89
|
-
self.base64_content = base64.b64encode(self.document_content)
|
|
90
|
-
|
|
91
|
-
yield # The test runs at this point
|
|
92
|
-
|
|
93
|
-
# --- Input Validation Tests ---
|
|
94
|
-
|
|
95
|
-
def test_llm_query_fails_if_no_company(self):
|
|
96
|
-
"""Tests that the query fails if the company does not exist."""
|
|
97
|
-
self.profile_repo.get_company_by_short_name.return_value = None
|
|
98
|
-
result = self.service.llm_query(company_short_name='a_company', question="test", external_user_id="test_user")
|
|
99
|
-
assert "No existe Company ID" in result["error_message"]
|
|
100
|
-
|
|
101
|
-
def test_llm_query_fails_if_no_question_or_prompt(self):
|
|
102
|
-
"""Tests that the query fails if both question and prompt_name are missing."""
|
|
103
|
-
result = self.service.llm_query(company_short_name='a_company', external_user_id="test_user")
|
|
104
|
-
assert "Hola, cual es tu pregunta?" in result["error_message"]
|
|
105
|
-
|
|
106
|
-
def test_llm_query_fails_if_no_previous_response_id_for_openai(self):
|
|
107
|
-
"""Tests query failure if no previous response ID is found for OpenAI models."""
|
|
108
|
-
self.session_context.get_last_response_id.return_value = None
|
|
109
|
-
self.llm_client_mock.set_company_context.return_value = None # Simulate that context initialization also fails
|
|
110
|
-
self.utility.is_openai_model.return_value = True
|
|
111
|
-
|
|
112
|
-
result = self.service.llm_query(
|
|
113
|
-
company_short_name='a_company',
|
|
114
|
-
external_user_id="test_user",
|
|
115
|
-
question="test"
|
|
116
|
-
)
|
|
117
|
-
assert "FATAL: No se encontró 'previous_response_id'" in result["error_message"]
|
|
118
|
-
|
|
119
|
-
# --- Core Logic Tests ---
|
|
120
|
-
|
|
121
|
-
def test_llm_query_with_direct_question_successfully(self):
|
|
122
|
-
"""Tests a direct query and the correct management of response IDs in the session."""
|
|
123
|
-
self.utility.is_openai_model.return_value = True
|
|
124
|
-
|
|
125
|
-
result = self.service.llm_query(company_short_name='test_company', question='hello',
|
|
126
|
-
external_user_id='test_user')
|
|
127
|
-
|
|
128
|
-
self.llm_client_mock.invoke.assert_called_once()
|
|
129
|
-
call_args = self.llm_client_mock.invoke.call_args.kwargs
|
|
130
|
-
assert call_args['company'] == self.company
|
|
131
|
-
assert call_args['user_identifier'] == 'test_user'
|
|
132
|
-
assert call_args['previous_response_id'] == 'prev_response_id'
|
|
133
|
-
assert "La pregunta que debes responder es: hello" in call_args['context']
|
|
134
|
-
|
|
135
|
-
self.session_context.save_last_response_id.assert_called_once_with(
|
|
136
|
-
'test_company', 'test_user', 'new_llm_response_id'
|
|
137
|
-
)
|
|
138
|
-
assert result['answer'] == 'LLM test response'
|
|
139
|
-
|
|
140
|
-
def test_llm_query_with_prompt_name_merges_data_correctly(self):
|
|
141
|
-
"""Tests that session data is merged with request data, with the latter taking priority."""
|
|
142
|
-
request_client_data = {'user_name': 'request_user'}
|
|
143
|
-
external_user_id = 'ext_user_2'
|
|
144
|
-
|
|
145
|
-
self.service.llm_query(
|
|
146
|
-
company_short_name='a_company',
|
|
147
|
-
external_user_id=external_user_id,
|
|
148
|
-
prompt_name="analisis_cartera",
|
|
149
|
-
client_data=request_client_data
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
self.llm_client_mock.invoke.assert_called_once()
|
|
153
|
-
call_kwargs = self.llm_client_mock.invoke.call_args.kwargs
|
|
154
|
-
|
|
155
|
-
expected_data = {
|
|
156
|
-
'prompt': 'analisis_cartera',
|
|
157
|
-
'data': {
|
|
158
|
-
'user_role': 'leader', # From session
|
|
159
|
-
'user_name': 'request_user', # From request (overwrites session)
|
|
160
|
-
'user_id': external_user_id # Added by the service
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
actual_question_dict = json.loads(call_kwargs['question'])
|
|
165
|
-
assert actual_question_dict == expected_data
|
|
166
|
-
|
|
167
|
-
# --- Context Initialization Tests ---
|
|
168
|
-
|
|
169
|
-
def test_llm_init_context_happy_path_external_user(self):
|
|
170
|
-
"""Tests the successful, full context initialization flow for an external user."""
|
|
171
|
-
self.utility.is_openai_model.return_value = True
|
|
172
|
-
|
|
173
|
-
self.service.llm_init_context(
|
|
174
|
-
company_short_name='test_co',
|
|
175
|
-
external_user_id='ext_user_123'
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
self.session_context.clear_all_context.assert_called_once_with(
|
|
179
|
-
company_short_name='test_co', user_identifier='ext_user_123'
|
|
180
|
-
)
|
|
181
|
-
self.session_context.save_user_session_data.assert_called_once_with(
|
|
182
|
-
'test_co', 'ext_user_123', self.dispatcher.get_user_info.return_value
|
|
183
|
-
)
|
|
184
|
-
self.llm_client_mock.set_company_context.assert_called_once()
|
|
185
|
-
self.session_context.save_last_response_id.assert_called_once_with(
|
|
186
|
-
'test_co', 'ext_user_123', 'new_context_response_id'
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
def test_llm_init_context_for_gemini_model(self):
|
|
190
|
-
"""Tests that for Gemini models, context is not sent to the LLM, but a flag is returned."""
|
|
191
|
-
self.utility.is_openai_model.return_value = False
|
|
192
|
-
self.utility.is_gemini_model.return_value = True
|
|
193
|
-
|
|
194
|
-
response = self.service.llm_init_context(
|
|
195
|
-
company_short_name='test_co',
|
|
196
|
-
external_user_id='ext_user_123',
|
|
197
|
-
model="gemini"
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
self.llm_client_mock.set_company_context.assert_not_called()
|
|
201
|
-
self.session_context.save_context_history.assert_called_once()
|
|
202
|
-
assert response == "gemini-context-initialized"
|
|
203
|
-
|
|
204
|
-
def test_llm_init_context_raises_exception_if_company_not_found(self):
|
|
205
|
-
"""Tests that an exception is raised if the company does not exist during context init."""
|
|
206
|
-
self.profile_repo.get_company_by_short_name.return_value = None
|
|
207
|
-
with pytest.raises(IAToolkitException, match="Empresa no encontrada: invalid_co"):
|
|
208
|
-
self.service.llm_init_context(company_short_name='invalid_co', external_user_id='user1')
|
|
209
|
-
|
|
210
|
-
# --- File Loading Tests ---
|
|
211
|
-
|
|
212
|
-
@patch('os.path.exists', return_value=False)
|
|
213
|
-
def test_load_files_for_context_handles_nonexistent_file(self, mock_exists):
|
|
214
|
-
"""Tests that a non-existent file is handled gracefully."""
|
|
215
|
-
result = self.service.load_files_for_context([{'file_id': 'nonexistent.txt'}])
|
|
216
|
-
assert "no fue encontrado y no pudo ser cargado" in result
|
|
217
|
-
|
|
218
|
-
@patch('os.path.exists', return_value=True)
|
|
219
|
-
def test_load_files_for_context_handles_service_exception(self, mock_exists):
|
|
220
|
-
"""Tests that exceptions from the document service are caught."""
|
|
221
|
-
self.document_service.file_to_txt.side_effect = Exception("Service failed")
|
|
222
|
-
files = [{'file_id': 'file.pdf', 'base64': self.base64_content.decode('utf-8')}]
|
|
223
|
-
result = self.service.load_files_for_context(files)
|
|
224
|
-
assert "Error al procesar el archivo file.pdf" in result
|
|
225
|
-
|
|
226
|
-
@patch('os.path.exists', return_value=True)
|
|
227
|
-
def test_load_files_for_context_builds_correctly(self, mock_exists):
|
|
228
|
-
"""Tests that file context is built correctly from base64 content."""
|
|
229
|
-
self.document_service.file_to_txt.return_value = "Text from file"
|
|
230
|
-
files = [{'file_id': 'test.pdf', 'base64': self.base64_content.decode('utf-8')}]
|
|
231
|
-
result = self.service.load_files_for_context(files)
|
|
232
|
-
|
|
233
|
-
self.document_service.file_to_txt.assert_called_once_with('test.pdf', self.document_content)
|
|
234
|
-
expected_context = """
|
|
235
|
-
A continuación encontraras una lista de documentos adjuntos
|
|
236
|
-
enviados por el usuario que hace la pregunta,
|
|
237
|
-
en total son: 1 documentos adjuntos
|
|
238
|
-
|
|
239
|
-
<document name='test.pdf'>
|
|
240
|
-
Text from file
|
|
241
|
-
</document>
|
|
242
|
-
"""
|
|
243
|
-
assert result == expected_context
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Product: IAToolkit
|
|
3
|
-
#
|
|
4
|
-
# IAToolkit is open source software.
|
|
5
|
-
|
|
6
|
-
from unittest.mock import MagicMock
|
|
7
|
-
from iatoolkit.services.search_service import SearchService
|
|
8
|
-
from iatoolkit.repositories.models import Document
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class TestSearchService:
|
|
12
|
-
def setup_method(self):
|
|
13
|
-
# Mock de dependencias
|
|
14
|
-
self.doc_repo = MagicMock()
|
|
15
|
-
self.vs_repo = MagicMock()
|
|
16
|
-
|
|
17
|
-
self.service = SearchService(
|
|
18
|
-
doc_repo=self.doc_repo,
|
|
19
|
-
vs_repo=self.vs_repo
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_search_documents_no_results(self):
|
|
24
|
-
self.vs_repo.query.return_value = []
|
|
25
|
-
|
|
26
|
-
result = self.service.search(company_id=1, query="consulta_inexistente")
|
|
27
|
-
assert result == ''
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_search_documents_success(self):
|
|
31
|
-
# Mock de chunks devueltos por el vector store
|
|
32
|
-
document = Document(id=1, company_id=1, filename='doc1.pdf', content="Contenido del documento")
|
|
33
|
-
self.vs_repo.query.return_value = [document]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Llamar a search y verificar resultado
|
|
37
|
-
result = self.service.search(company_id=1, query="consulta")
|
|
38
|
-
|
|
39
|
-
assert result == 'documento "doc1.pdf": Contenido del documento\n'
|
|
@@ -1,160 +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
|
|
8
|
-
from sqlalchemy import text # Para verificar el argumento de text()
|
|
9
|
-
import json
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
from iatoolkit.services.sql_service import SqlService
|
|
12
|
-
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
13
|
-
from iatoolkit.common.util import Utility
|
|
14
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
15
|
-
|
|
16
|
-
class TestSqlService:
|
|
17
|
-
def setup_method(self):
|
|
18
|
-
|
|
19
|
-
self.db_manager_mock = MagicMock(spec=DatabaseManager)
|
|
20
|
-
self.util_mock = MagicMock(spec=Utility)
|
|
21
|
-
|
|
22
|
-
# Mock para la sesión de base de datos y el objeto de resultado de la consulta
|
|
23
|
-
self.session_mock = MagicMock()
|
|
24
|
-
self.mock_result_proxy = MagicMock() # Simula el objeto ResultProxy de SQLAlchemy
|
|
25
|
-
|
|
26
|
-
# Configurar los mocks para que devuelvan otros mocks cuando sea necesario
|
|
27
|
-
self.db_manager_mock.get_session.return_value = self.session_mock
|
|
28
|
-
self.session_mock.execute.return_value = self.mock_result_proxy
|
|
29
|
-
|
|
30
|
-
# Instanciar el servicio con las dependencias mockeadas
|
|
31
|
-
self.service = SqlService(util=self.util_mock)
|
|
32
|
-
|
|
33
|
-
def test_exec_sql_success_with_simple_data(self):
|
|
34
|
-
"""
|
|
35
|
-
Prueba la ejecución exitosa de una consulta SQL que devuelve datos simples (int, str).
|
|
36
|
-
En este caso, la función de serialización personalizada no debería ser invocada para estos tipos.
|
|
37
|
-
"""
|
|
38
|
-
sql_statement = "SELECT id, name FROM users WHERE status = 'active'"
|
|
39
|
-
expected_keys = ['id', 'name']
|
|
40
|
-
# Datos que json.dumps puede manejar directamente
|
|
41
|
-
expected_rows_from_db = [(1, 'Alice'), (2, 'Bob')]
|
|
42
|
-
# Cómo se verán los datos después del procesamiento interno en exec_sql antes de json.dumps
|
|
43
|
-
expected_rows_as_dicts = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
|
|
44
|
-
|
|
45
|
-
self.mock_result_proxy.keys.return_value = expected_keys
|
|
46
|
-
self.mock_result_proxy.fetchall.return_value = expected_rows_from_db
|
|
47
|
-
|
|
48
|
-
# Ejecutar el método a probar
|
|
49
|
-
result_json = self.service.exec_sql(self.db_manager_mock, sql_statement)
|
|
50
|
-
|
|
51
|
-
# Verificar que la sesión y la ejecución fueron llamadas correctamente
|
|
52
|
-
self.db_manager_mock.get_session.assert_called_once()
|
|
53
|
-
self.session_mock.execute.assert_called_once()
|
|
54
|
-
# Verificar que el argumento de execute fue el objeto text(sql_statement)
|
|
55
|
-
args, _ = self.session_mock.execute.call_args
|
|
56
|
-
assert isinstance(args[0], type(text(""))) # Comprueba el tipo del argumento
|
|
57
|
-
assert str(args[0]) == sql_statement # Comprueba el contenido del SQL
|
|
58
|
-
|
|
59
|
-
self.mock_result_proxy.keys.assert_called_once()
|
|
60
|
-
self.mock_result_proxy.fetchall.assert_called_once()
|
|
61
|
-
|
|
62
|
-
# Verificar que la función de serialización personalizada no fue llamada
|
|
63
|
-
# para tipos que json.dumps maneja nativamente.
|
|
64
|
-
self.util_mock.serialize.assert_not_called()
|
|
65
|
-
|
|
66
|
-
# Verificar el resultado JSON
|
|
67
|
-
expected_json_output = json.dumps(expected_rows_as_dicts)
|
|
68
|
-
assert result_json == expected_json_output
|
|
69
|
-
|
|
70
|
-
def test_exec_sql_success_with_custom_data_type_serialization(self):
|
|
71
|
-
"""
|
|
72
|
-
Prueba la ejecución exitosa de una consulta SQL que devuelve datos que requieren
|
|
73
|
-
serialización personalizada (ej. un objeto datetime).
|
|
74
|
-
"""
|
|
75
|
-
sql_statement = "SELECT event_name, event_time FROM important_events"
|
|
76
|
-
original_datetime_obj = datetime(2024, 1, 15, 10, 30, 0)
|
|
77
|
-
# Suponemos que util.serialize convierte datetime a una string ISO
|
|
78
|
-
serialized_datetime_str = original_datetime_obj.isoformat()
|
|
79
|
-
|
|
80
|
-
expected_keys = ['event_name', 'event_time']
|
|
81
|
-
# La base de datos devuelve una tupla con un objeto datetime
|
|
82
|
-
expected_rows_from_db = [('Team Meeting', original_datetime_obj)]
|
|
83
|
-
# El diccionario que se pasará a json.dumps, después de que serialize haga su trabajo
|
|
84
|
-
expected_rows_as_dicts_after_serialization = [{'event_name': 'Team Meeting', 'event_time': serialized_datetime_str}]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.mock_result_proxy.keys.return_value = expected_keys
|
|
88
|
-
self.mock_result_proxy.fetchall.return_value = expected_rows_from_db
|
|
89
|
-
|
|
90
|
-
# Configurar el mock de util.serialize para que maneje datetime
|
|
91
|
-
def mock_serializer(obj):
|
|
92
|
-
if isinstance(obj, datetime):
|
|
93
|
-
return obj.isoformat()
|
|
94
|
-
# Para cualquier otro tipo, podría devolverlo tal cual si json.dumps lo maneja,
|
|
95
|
-
# o lanzar un TypeError como lo haría json.dumps si default no estuviera.
|
|
96
|
-
return obj
|
|
97
|
-
self.util_mock.serialize.side_effect = mock_serializer
|
|
98
|
-
|
|
99
|
-
# Ejecutar el método a probar
|
|
100
|
-
result_json = self.service.exec_sql(self.db_manager_mock, sql_statement)
|
|
101
|
-
|
|
102
|
-
# Verificar llamadas
|
|
103
|
-
self.session_mock.execute.assert_called_once()
|
|
104
|
-
self.mock_result_proxy.keys.assert_called_once()
|
|
105
|
-
self.mock_result_proxy.fetchall.assert_called_once()
|
|
106
|
-
|
|
107
|
-
# Verificar que util.serialize fue llamado para el objeto datetime
|
|
108
|
-
self.util_mock.serialize.assert_called_once_with(original_datetime_obj)
|
|
109
|
-
|
|
110
|
-
# Verificar el resultado JSON
|
|
111
|
-
expected_json_output = json.dumps(expected_rows_as_dicts_after_serialization)
|
|
112
|
-
assert result_json == expected_json_output
|
|
113
|
-
|
|
114
|
-
def test_exec_sql_success_no_results(self):
|
|
115
|
-
"""
|
|
116
|
-
Prueba la ejecución de una consulta SQL que no devuelve resultados.
|
|
117
|
-
"""
|
|
118
|
-
sql_statement = "SELECT id FROM users WHERE name = 'NonExistentUser'"
|
|
119
|
-
expected_keys = ['id'] # Las claves se devuelven incluso sin filas
|
|
120
|
-
expected_rows_from_db = []
|
|
121
|
-
expected_rows_as_dicts = []
|
|
122
|
-
|
|
123
|
-
self.mock_result_proxy.keys.return_value = expected_keys
|
|
124
|
-
self.mock_result_proxy.fetchall.return_value = expected_rows_from_db
|
|
125
|
-
|
|
126
|
-
result_json = self.service.exec_sql(self.db_manager_mock, sql_statement)
|
|
127
|
-
|
|
128
|
-
self.session_mock.execute.assert_called_once()
|
|
129
|
-
self.mock_result_proxy.keys.assert_called_once()
|
|
130
|
-
self.mock_result_proxy.fetchall.assert_called_once()
|
|
131
|
-
self.util_mock.serialize.assert_not_called() # No hay datos que necesiten serialización
|
|
132
|
-
|
|
133
|
-
expected_json_output = json.dumps(expected_rows_as_dicts, indent=2)
|
|
134
|
-
assert result_json == expected_json_output
|
|
135
|
-
|
|
136
|
-
def test_exec_sql_raises_app_exception_on_database_error(self):
|
|
137
|
-
"""
|
|
138
|
-
Prueba que se lanza una IAToolkitException cuando ocurre un error en la base de datos.
|
|
139
|
-
"""
|
|
140
|
-
sql_statement = "SELECT * FROM table_that_does_not_exist"
|
|
141
|
-
original_db_error_message = "Error: Table not found"
|
|
142
|
-
# Configurar el mock para que lance una excepción cuando se llame a execute
|
|
143
|
-
self.session_mock.execute.side_effect = Exception(original_db_error_message)
|
|
144
|
-
|
|
145
|
-
with pytest.raises(IAToolkitException) as exc_info:
|
|
146
|
-
self.service.exec_sql(self.db_manager_mock, sql_statement)
|
|
147
|
-
|
|
148
|
-
# Verificar el tipo de excepción y el mensaje
|
|
149
|
-
assert exc_info.value.error_type == IAToolkitException.ErrorType.DATABASE_ERROR
|
|
150
|
-
assert original_db_error_message in str(exc_info.value)
|
|
151
|
-
# Verificar que la excepción original está encadenada (from e)
|
|
152
|
-
assert isinstance(exc_info.value.__cause__, Exception)
|
|
153
|
-
assert str(exc_info.value.__cause__) == original_db_error_message
|
|
154
|
-
|
|
155
|
-
# Verificar que se intentó ejecutar la consulta
|
|
156
|
-
self.session_mock.execute.assert_called_once()
|
|
157
|
-
# Otros mocks no deberían haber sido llamados si execute falló
|
|
158
|
-
self.mock_result_proxy.keys.assert_not_called()
|
|
159
|
-
self.mock_result_proxy.fetchall.assert_not_called()
|
|
160
|
-
self.util_mock.serialize.assert_not_called()
|