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,85 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- import pytest
7
- import os
8
- from unittest.mock import patch, mock_open, call
9
- from iatoolkit.infra.connectors.local_file_connector import LocalFileConnector
10
- from iatoolkit.common.exceptions import IAToolkitException
11
- from datetime import datetime
12
-
13
-
14
- class TestLocalFileConnector:
15
- def setup_method(self):
16
- self.mock_directory = "/mock/directory"
17
- self.file_connector = LocalFileConnector(self.mock_directory)
18
-
19
- @patch("os.listdir", side_effect=Exception("Error al listar directorio"))
20
- def test_list_files_error(self, mock_listdir):
21
- with pytest.raises(IAToolkitException) as excinfo:
22
- self.file_connector.list_files()
23
-
24
- assert excinfo.value.error_type == IAToolkitException.ErrorType.FILE_IO_ERROR
25
- assert "Error procesando el directorio" in str(excinfo.value)
26
- mock_listdir.assert_called_once_with(self.mock_directory)
27
-
28
- @patch("os.path.getmtime")
29
- @patch("os.path.getsize")
30
- @patch("os.listdir")
31
- @patch("os.path.isfile")
32
- def test_list_files_success(self, mock_isfile, mock_listdir,
33
- mock_getsize, mock_getmtime):
34
- # Configurar mocks
35
- mock_listdir.return_value = ["file1.txt", "file2.pdf", "subdir"]
36
- mock_isfile.side_effect = lambda path: not path.endswith("subdir")
37
- mock_getsize.return_value = 100
38
- mock_getmtime.return_value = datetime(2024, 2, 19, 15, 30)
39
-
40
- expected_return = [
41
- {
42
- 'name': "file1.txt", 'path': '/mock/directory/file1.txt',
43
- 'metadata': {'size': 100, 'last_modified': mock_getmtime.return_value}
44
- },
45
- {
46
- 'name': "file2.pdf", 'path': '/mock/directory/file2.pdf',
47
- 'metadata': {'size': 100, 'last_modified': mock_getmtime.return_value}
48
- }
49
- ]
50
-
51
-
52
- result = self.file_connector.list_files()
53
-
54
- assert result == expected_return
55
- mock_listdir.assert_called_once_with(self.mock_directory)
56
- mock_isfile.assert_has_calls([
57
- call(os.path.join(self.mock_directory, "file1.txt")),
58
- call(os.path.join(self.mock_directory, "file2.pdf")),
59
- call(os.path.join(self.mock_directory, "subdir")),
60
- ])
61
-
62
-
63
- @patch("builtins.open", side_effect=Exception("Error al abrir el archivo"))
64
- def test_get_file_content_error(self, mock_open_file):
65
- """Prueba para verificar que `get_file_content` lanza una excepción en caso de error."""
66
- mock_file_path = os.path.join(self.mock_directory, "file1.txt")
67
-
68
- # Verificar que se lanza la excepción esperada
69
- with pytest.raises(IAToolkitException) as excinfo:
70
- self.file_connector.get_file_content(mock_file_path)
71
-
72
- assert excinfo.value.error_type == IAToolkitException.ErrorType.FILE_IO_ERROR
73
- assert "Error leyendo el archivo" in str(excinfo.value)
74
- mock_open_file.assert_called_once_with(mock_file_path, "rb")
75
-
76
- @patch("builtins.open", new_callable=mock_open, read_data=b"file content")
77
- def test_get_file_content_success(self, mock_open_file):
78
- mock_file_path = os.path.join(self.mock_directory, "file1.txt")
79
-
80
- result = self.file_connector.get_file_content(mock_file_path)
81
-
82
- # Verificaciones
83
- assert result == b"file content"
84
- mock_open_file.assert_called_once_with(mock_file_path, "rb")
85
-
@@ -1,95 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- import unittest
7
- from unittest.mock import patch, MagicMock
8
- from iatoolkit.infra.connectors.s3_connector import S3Connector
9
-
10
-
11
- class TestS3Connector(unittest.TestCase):
12
- def setUp(self):
13
- # Mock de `boto3.client`
14
- self.boto3_client_patch = patch('iatoolkit.infra.connectors.s3_connector.boto3.client')
15
- self.mock_boto3_client = self.boto3_client_patch.start()
16
-
17
- # Mock del cliente S3
18
- self.mock_s3_client = MagicMock()
19
- self.mock_boto3_client.return_value = self.mock_s3_client
20
-
21
- # Configuración básica
22
- self.bucket = "test-bucket"
23
- self.prefix = "test-prefix/"
24
- self.auth = {
25
- "aws_access_key_id": "mock-key",
26
- "aws_secret_access_key": "mock-secret",
27
- "region_name": "us-east-1"
28
- }
29
-
30
- # Crear una instancia del conector con los mocks
31
- self.connector = S3Connector(bucket=self.bucket, prefix=self.prefix, auth=self.auth)
32
-
33
- def tearDown(self):
34
- self.boto3_client_patch.stop()
35
-
36
- def test_init_when_boto3_client_error(self):
37
- self.mock_boto3_client.side_effect = Exception("Failed to create boto3 client")
38
-
39
- with self.assertRaises(Exception) as context:
40
- S3Connector(bucket=self.bucket, prefix=self.prefix, auth=self.auth)
41
-
42
- # Validar que el mensaje de la excepción coincide
43
- self.assertEqual(str(context.exception), "Failed to create boto3 client")
44
-
45
- def test_list_files_empty(self):
46
- self.mock_s3_client.list_objects_v2.return_value = {}
47
-
48
- result = self.connector.list_files()
49
- self.assertEqual(result, [])
50
-
51
- def test_list_files_success(self):
52
- # Simulación de respuesta de `list_objects_v2`
53
- self.mock_s3_client.list_objects_v2.return_value = {
54
- "Contents": [
55
- {
56
- "Key": "test-prefix/file1.txt",
57
- "Size": 1024,
58
- "LastModified": "2023-01-01T12:00:00.000Z"
59
- },
60
- {
61
- "Key": "test-prefix/file2.txt",
62
- "Size": 2048,
63
- "LastModified": "2023-01-02T12:00:00.000Z"
64
- }
65
- ]
66
- }
67
-
68
- result = self.connector.list_files()
69
-
70
- self.assertEqual(len(result), 2)
71
- self.assertEqual(result[0]['path'], "test-prefix/file1.txt")
72
- self.assertEqual(result[0]['name'], "file1.txt")
73
- self.assertEqual(result[0]['metadata']['size'], 1024)
74
- self.assertEqual(result[0]['metadata']['last_modified'], "2023-01-01T12:00:00.000Z")
75
-
76
-
77
- def test_get_file_content_not_found(self):
78
- # Simulación de excepción de archivo no encontrado
79
- self.mock_s3_client.get_object.side_effect = Exception("NoSuchKey")
80
-
81
- # Llamada al método y capturar excepción
82
- with self.assertRaises(Exception) as context:
83
- self.connector.get_file_content("test-prefix/nonexistent.txt")
84
-
85
- # Validar la excepción
86
- self.assertEqual(str(context.exception), "NoSuchKey")
87
-
88
- def test_get_file_content_success(self):
89
- # Simulación de respuesta de `get_object`
90
- mock_body = MagicMock()
91
- mock_body.read.return_value = b"mock file content"
92
- self.mock_s3_client.get_object.return_value = {"Body": mock_body}
93
-
94
- result = self.connector.get_file_content("test-prefix/file1.txt")
95
- self.assertEqual(result, b"mock file content")
@@ -1,92 +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 patch, MagicMock
8
- from iatoolkit.infra.call_service import CallServiceClient
9
- from requests import RequestException
10
- from iatoolkit.common.exceptions import IAToolkitException
11
-
12
-
13
- class TestCallServiceClient:
14
-
15
- def setup_method(self):
16
- self.client = CallServiceClient()
17
- self.endpoint = "http://fake-endpoint.com/api/test"
18
- self.mock_response = MagicMock()
19
- self.mock_response.json.return_value = {'result': 'ok'}
20
- self.mock_response.status_code = 200
21
-
22
- # Patch 'requests' methods
23
- self.get_patcher = patch('iatoolkit.infra.call_service.requests.get', return_value=self.mock_response)
24
- self.post_patcher = patch('iatoolkit.infra.call_service.requests.post', return_value=self.mock_response)
25
- self.put_patcher = patch('iatoolkit.infra.call_service.requests.put', return_value=self.mock_response)
26
- self.patch_patcher = patch('iatoolkit.infra.call_service.requests.patch', return_value=self.mock_response)
27
- self.delete_patcher = patch('iatoolkit.infra.call_service.requests.delete', return_value=self.mock_response)
28
-
29
- # Start patching
30
- self.mock_get = self.get_patcher.start()
31
- self.mock_post = self.post_patcher.start()
32
- self.mock_put = self.put_patcher.start()
33
- self.mock_patch = self.patch_patcher.start()
34
- self.mock_delete = self.delete_patcher.start()
35
-
36
- def teardown_method(self):
37
- patch.stopall()
38
-
39
- def test_get_success(self):
40
- response, status = self.client.get(self.endpoint)
41
- self.mock_get.assert_called_once_with(self.endpoint,
42
- headers= {'Content-Type': 'application/json'},
43
- params=None, timeout=(10, 10.0))
44
- assert status == 200
45
- assert response == {'result': 'ok'}
46
-
47
- def test_get_failure(self):
48
- self.mock_get.side_effect = RequestException("Failed GET")
49
-
50
- with pytest.raises(IAToolkitException) as exc_info:
51
- self.client.get(self.endpoint)
52
-
53
- assert exc_info.value.error_type == IAToolkitException.ErrorType.REQUEST_ERROR
54
-
55
- def test_post_success(self):
56
- json_dict = {'key': 'value'}
57
- response, status = self.client.post(self.endpoint, json_dict)
58
- self.mock_post.assert_called_once_with(self.endpoint, json=json_dict,
59
- headers=self.client.headers,
60
- params=None,
61
- timeout=(10, 10.0))
62
- assert status == 200
63
- assert response == {'result': 'ok'}
64
-
65
- def test_post_failure(self):
66
- self.mock_post.side_effect = RequestException("Failed POST")
67
-
68
- with pytest.raises(IAToolkitException) as exc_info:
69
- self.client.post(self.endpoint, {'data': 'test'})
70
-
71
- assert exc_info.value.error_type == IAToolkitException.ErrorType.REQUEST_ERROR
72
-
73
- def test_put_success(self):
74
- json_dict = {'key': 'updated'}
75
- response, status = self.client.put(self.endpoint, json_dict)
76
- assert status == 200
77
- assert response == {'result': 'ok'}
78
-
79
-
80
- def test_delete_success(self):
81
- json_dict = {'key': 'deleted'}
82
- response, status = self.client.delete(self.endpoint, json_dict)
83
-
84
- assert status == 200
85
- assert response == {'result': 'ok'}
86
-
87
- def test_post_files_success(self):
88
- files = {'file': ('filename.txt', 'filecontent')}
89
- response, status = self.client.post_files(self.endpoint, files)
90
- assert status == 200
91
- assert response == {'result': 'ok'}
92
-
@@ -1,59 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- import unittest
7
- from unittest.mock import patch, MagicMock
8
- from iatoolkit.repositories.database_manager import DatabaseManager
9
-
10
-
11
- class TestDatabaseManager(unittest.TestCase):
12
-
13
- @patch('iatoolkit.repositories.database_manager.create_engine')
14
- @patch('iatoolkit.repositories.database_manager.sessionmaker')
15
- @patch('iatoolkit.repositories.database_manager.scoped_session')
16
- def setUp(self, mock_scoped_session, mock_sessionmaker, mock_create_engine):
17
- """Configura mocks para las dependencias principales de DatabaseManager."""
18
- # Mock del motor y las sesiones
19
- self.mock_engine = MagicMock()
20
- mock_create_engine.return_value = self.mock_engine
21
-
22
- self.mock_sessionmaker = MagicMock()
23
- mock_sessionmaker.return_value = self.mock_sessionmaker
24
-
25
- self.mock_scoped_session = MagicMock()
26
- mock_scoped_session.return_value = self.mock_scoped_session
27
-
28
- # Instanciar el DatabaseManager con un mock de URL de base de datos
29
- self.db_manager = DatabaseManager("sqlite:///:memory:")
30
-
31
- # Verificaciones iniciales
32
- mock_create_engine.assert_called_once_with("sqlite:///:memory:", echo=False)
33
- mock_sessionmaker.assert_called_once_with(bind=self.mock_engine)
34
- mock_scoped_session.assert_called_once_with(self.mock_sessionmaker)
35
-
36
- def test_get_session(self):
37
- """Prueba que get_session devuelve una nueva sesión."""
38
- session = self.db_manager.get_session()
39
- self.mock_scoped_session.assert_called_once()
40
- self.assertEqual(session, self.mock_scoped_session())
41
-
42
- @patch('iatoolkit.repositories.database_manager.Base.metadata.create_all')
43
- def test_create_all(self, mock_create_all):
44
- """Prueba que create_all crea las tablas usando la metadata de Base."""
45
- self.db_manager.create_all()
46
- mock_create_all.assert_called_once_with(self.mock_engine)
47
-
48
- @patch('iatoolkit.repositories.database_manager.Base.metadata.drop_all')
49
- def test_drop_all(self, mock_drop_all):
50
- """Prueba que drop_all elimina las tablas usando la metadata de Base."""
51
- self.db_manager.drop_all()
52
- mock_drop_all.assert_called_once_with(self.mock_engine)
53
-
54
- def test_remove_session(self):
55
- """Prueba que remove_session limpia la sesión actual."""
56
- self.db_manager.remove_session()
57
- self.mock_scoped_session.remove.assert_called_once()
58
-
59
-
@@ -1,137 +0,0 @@
1
- import pytest
2
- from unittest.mock import patch, MagicMock
3
- import uuid
4
- import json
5
-
6
- from iatoolkit.infra.gemini_adapter import GeminiAdapter
7
- from iatoolkit.infra.llm_response import LLMResponse, ToolCall
8
- from iatoolkit.common.exceptions import IAToolkitException
9
-
10
-
11
- class TestGeminiAdapter:
12
- """Tests para la clase GeminiAdapter."""
13
-
14
- def setup_method(self):
15
- """Configura el entorno de prueba antes de cada test."""
16
- self.mock_gemini_client = MagicMock()
17
- self.mock_generative_model = MagicMock()
18
- self.mock_gemini_client.GenerativeModel.return_value = self.mock_generative_model
19
- self.adapter = GeminiAdapter(gemini_client=self.mock_gemini_client)
20
-
21
- patch('iatoolkit.infra.gemini_adapter.uuid.uuid4', return_value=uuid.UUID('12345678-1234-5678-1234-567812345678')).start()
22
-
23
- self.message_to_dict_patcher = patch('iatoolkit.infra.gemini_adapter.MessageToDict')
24
- self.mock_message_to_dict = self.message_to_dict_patcher.start()
25
-
26
- def teardown_method(self):
27
- patch.stopall()
28
-
29
- def _create_mock_gemini_response(self, text_content=None, function_call=None, finish_reason="STOP",
30
- usage_metadata=None):
31
- """Crea un objeto de respuesta mock de Gemini de forma robusta."""
32
- mock_response = MagicMock()
33
- parts = []
34
-
35
- if text_content:
36
- part = MagicMock()
37
- part.text = text_content
38
- # Nos aseguramos que el otro atributo no exista para evitar ambigüedad
39
- del part.function_call
40
- parts.append(part)
41
-
42
- if function_call:
43
- # Crea un mock para el objeto `function_call` y asigna sus atributos directamente.
44
- mock_fc_obj = MagicMock()
45
- mock_fc_obj.name = function_call['name']
46
- mock_fc_obj._pb = "mock_pb" # Simular el objeto protobuf interno
47
-
48
- # Configura el mock del conversor para que devuelva los args esperados
49
- self.mock_message_to_dict.return_value = {'args': function_call['args']}
50
-
51
- part = MagicMock()
52
- part.function_call = mock_fc_obj
53
- del part.text
54
- parts.append(part)
55
-
56
- mock_candidate = MagicMock()
57
- mock_candidate.content.parts = parts
58
- mock_candidate.finish_reason = finish_reason
59
- mock_response.candidates = [mock_candidate]
60
-
61
- if usage_metadata:
62
- mock_response.usage_metadata = MagicMock(**usage_metadata)
63
- else:
64
- del mock_response.usage_metadata
65
-
66
- return mock_response
67
-
68
- def test_create_response_text_only(self):
69
- """Prueba una llamada simple que devuelve solo texto."""
70
- mock_response = self._create_mock_gemini_response(text_content="Hola mundo")
71
- self.mock_generative_model.generate_content.return_value = mock_response
72
-
73
- response = self.adapter.create_response(model="gemini-pro", input=[])
74
-
75
- assert isinstance(response, LLMResponse)
76
- assert response.output_text == "Hola mundo"
77
- assert len(response.output) == 0
78
-
79
- def test_create_response_text_with_history(self):
80
- """Prueba una llamada simple que devuelve solo texto."""
81
- mock_response = self._create_mock_gemini_response(text_content="Hola mundo")
82
- self.mock_generative_model.generate_content.return_value = mock_response
83
-
84
- context_history = [{"role": "user", "content": "Pregunta"}]
85
-
86
- response = self.adapter.create_response(model="gemini-pro",
87
- input=[],
88
- context_history=context_history)
89
-
90
- assert isinstance(response, LLMResponse)
91
- assert response.output_text == "Hola mundo"
92
- assert len(context_history) == 2
93
-
94
- def test_create_response_with_tool_call(self):
95
- """Prueba una llamada que devuelve una function_call."""
96
- func_call_data = {'name': 'get_weather', 'args': {'location': 'Santiago'}}
97
- mock_response = self._create_mock_gemini_response(function_call=func_call_data)
98
- self.mock_generative_model.generate_content.return_value = mock_response
99
-
100
- response = self.adapter.create_response(model="gemini-flash", input=[], tools=[{}])
101
-
102
- assert len(response.output) == 1
103
- tool_call = response.output[0]
104
- assert isinstance(tool_call, ToolCall)
105
- assert tool_call.name == "get_weather"
106
- assert tool_call.arguments == json.dumps(func_call_data['args'])
107
- self.mock_message_to_dict.assert_called_once_with("mock_pb")
108
-
109
- def test_history_not_modified_if_no_content_in_response(self):
110
- """Prueba que el historial no se modifica si la respuesta está vacía."""
111
- mock_response = self._create_mock_gemini_response() # Sin texto ni tool calls
112
- self.mock_generative_model.generate_content.return_value = mock_response
113
-
114
- context_history = [{"role": "user", "content": "Pregunta"}]
115
- self.adapter.create_response(model="gemini-pro", input=[], context_history=context_history)
116
-
117
- assert len(context_history) == 1 # El historial no debe cambiar
118
-
119
- @pytest.mark.parametrize("error_msg, expected_app_msg", [
120
- ("Quota exceeded", "Se ha excedido la cuota de la API de Gemini"),
121
- ("Content blocked", "El contenido fue bloqueado"),
122
- ("Invalid token", "Tu consulta supera el límite de contexto de Gemini"),
123
- ("Other API error", "Error calling Gemini API: Other API error"),
124
- ])
125
- def test_api_error_handling(self, error_msg, expected_app_msg):
126
- self.mock_generative_model.generate_content.side_effect = Exception(error_msg)
127
- with pytest.raises(IAToolkitException, match=expected_app_msg):
128
- self.adapter.create_response(model="gemini-pro", input=[])
129
-
130
- @pytest.mark.parametrize("finish_reason, expected_status", [
131
- ("SAFETY", "blocked"), ("MAX_TOKENS", "length_exceeded"), ("STOP", "completed")
132
- ])
133
- def test_map_finish_reason_to_status(self, finish_reason, expected_status):
134
- mock_response = self._create_mock_gemini_response(finish_reason=finish_reason)
135
- self.mock_generative_model.generate_content.return_value = mock_response
136
- response = self.adapter.create_response(model="gemini-pro", input=[])
137
- assert response.status == expected_status
@@ -1,68 +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 Mock, patch
8
- from iatoolkit.infra.google_chat_app import GoogleChatApp
9
-
10
-
11
- class TestGoogleChatApp:
12
- @pytest.fixture
13
- def mock_call_service(self):
14
- return Mock()
15
-
16
- @pytest.fixture
17
- def google_chat_service(self, mock_call_service):
18
- return GoogleChatApp(mock_call_service)
19
-
20
- def test_send_message_success(self, google_chat_service, mock_call_service):
21
- # Configurar el mock
22
- mock_call_service.post.return_value = ({"status": "sent"}, 200)
23
-
24
- message_data = {
25
- "type": "MESSAGE_TRIGGER",
26
- "space": {
27
- "name": "spaces/AAQAupQldd4"
28
- },
29
- "message": {
30
- "text": "Test message"
31
- }
32
- }
33
-
34
- with patch.dict('os.environ', {'GOOGLE_CHAT_BOT_URL': 'https://test-bot.com'}):
35
- result = google_chat_service.send_message(message_data=message_data)
36
-
37
- assert result['success'] is True
38
- assert result['message'] == "Mensaje enviado correctamente"
39
- mock_call_service.post.assert_called_once_with('https://test-bot.com', message_data)
40
-
41
- def test_send_message_missing_env_var(self, google_chat_service):
42
- message_data = {
43
- "type": "MESSAGE_TRIGGER",
44
- "space": {"name": "spaces/test"},
45
- "message": {"text": "Test"}
46
- }
47
-
48
- with patch.dict('os.environ', {}, clear=True):
49
- result = google_chat_service.send_message(message_data=message_data)
50
-
51
- assert result['success'] is False
52
- assert "GOOGLE_CHAT_BOT_URL no está configurada" in result['message']
53
-
54
- def test_send_message_api_error(self, google_chat_service, mock_call_service):
55
- # Configurar el mock para simular error
56
- mock_call_service.post.return_value = ({"error": "API Error"}, 500)
57
-
58
- message_data = {
59
- "type": "MESSAGE_TRIGGER",
60
- "space": {"name": "spaces/test"},
61
- "message": {"text": "Test"}
62
- }
63
-
64
- with patch.dict('os.environ', {'GOOGLE_CHAT_BOT_URL': 'https://test-bot.com'}):
65
- result = google_chat_service.send_message(message_data=message_data)
66
-
67
- assert result['success'] is False
68
- assert "Error al enviar mensaje" in result['message']