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,76 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from iatoolkit.repositories.tasks_repo import TaskRepo
7
- from iatoolkit.repositories.profile_repo import ProfileRepo
8
- from iatoolkit.repositories.models import Task, TaskType, Company, TaskStatus
9
- from iatoolkit.repositories.database_manager import DatabaseManager
10
-
11
-
12
- class TestTaskRepo:
13
- def setup_method(self):
14
- self.db_manager = DatabaseManager('sqlite:///:memory:')
15
- self.db_manager.create_all()
16
- self.session = self.db_manager.get_session()
17
- self.profile_repo = ProfileRepo(self.db_manager)
18
-
19
-
20
- self.company = Company(name='opensoft', short_name='open')
21
- self.profile_repo.create_company(self.company)
22
-
23
- self.task_type = TaskType(name="dummy_type")
24
- self.task = Task(id=1, task_type_id=1, company_id=self.company.id)
25
-
26
- self.repo = TaskRepo(self.db_manager)
27
-
28
-
29
- def test_create_task(self):
30
- result = self.repo.create_task(self.task)
31
-
32
- task_in_db = self.session.query(Task).filter_by(id=self.task.id).first()
33
-
34
- assert task_in_db is not None
35
- assert task_in_db.id == self.task.id
36
- assert task_in_db.task_type_id == self.task.task_type_id
37
- assert task_in_db.company_id == self.task.company_id
38
- assert result == task_in_db
39
-
40
- def test_update_task(self):
41
- self.repo.create_task(self.task)
42
-
43
- # update value
44
- self.task.client_data = {'key': 'value'}
45
- result = self.repo.update_task(self.task)
46
-
47
- task_in_db = self.session.query(Task).filter_by(id=self.task.id).first()
48
- assert task_in_db.client_data == self.task.client_data
49
-
50
- def test_create_or_update_task_type_creates_new(self):
51
- result = self.repo.create_or_update_task_type(self.task_type)
52
- assert result == self.task_type
53
-
54
- def test_create_or_update_task_type_updates_existing(self):
55
- existing_type = TaskType(name="dummy_type", prompt_template="old_template", template_args="old_args")
56
- self.repo.create_or_update_task_type(existing_type)
57
-
58
-
59
- new_type = TaskType(name="dummy_type", prompt_template="new_template", template_args="new_args")
60
- result = self.repo.create_or_update_task_type(new_type)
61
-
62
- assert existing_type.prompt_template == "new_template"
63
- assert existing_type.template_args == "new_args"
64
- assert result == existing_type
65
-
66
- def test_get_task_type_when_found(self):
67
- result = self.repo.get_task_type("dummy_type")
68
- assert result == None
69
-
70
- def test_get_pending_tasks(self):
71
- self.task.status = TaskStatus.pendiente
72
- self.repo.create_task(self.task)
73
-
74
- result = self.repo.get_pending_tasks(self.company.id)
75
- assert len(result) == 1
76
-
@@ -1,107 +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.repositories.vs_repo import VSRepo
10
- from iatoolkit.repositories.models import VSDoc, Document
11
-
12
-
13
- class TestVSRepo:
14
- @pytest.fixture
15
- def mock_db_manager(self):
16
- """Fixture para mockear el DatabaseManager."""
17
- mock_manager = MagicMock()
18
- mock_session = MagicMock()
19
- mock_manager.get_session.return_value = mock_session
20
- return mock_manager
21
-
22
- @pytest.fixture
23
- def mock_embedder(self):
24
- """Fixture para mockear el modelo de SentenceTransformer."""
25
- with patch('iatoolkit.repositories.vs_repo.InferenceClient') as MockEmbedder:
26
- mock_instance = MockEmbedder.return_value
27
- mock_instance.feature_extraction.return_value = [0.1, 0.2, 0.3] # Retorna un embedding simulado
28
- yield mock_instance
29
-
30
- @pytest.fixture
31
- def vs_repo(self, mock_db_manager, mock_embedder):
32
- """Fixture para inicializar el repositorio VSRepo con dependencias mockeadas."""
33
- return VSRepo(mock_db_manager)
34
-
35
- def test_add_document_rollback_on_error(self, vs_repo, mock_db_manager, mock_embedder):
36
- """Prueba que verifica el rollback cuando ocurre un error en add_document."""
37
- mock_session = mock_db_manager.get_session.return_value
38
-
39
- mock_embedder.feature_extraction.side_effect = Exception("Error al generar embeddings")
40
- vs_chunk_list = [VSDoc(id=1, text="Documento con error")]
41
-
42
- with pytest.raises(IAToolkitException) as excinfo:
43
- vs_repo.add_document(vs_chunk_list)
44
-
45
- assert excinfo.value.error_type == IAToolkitException.ErrorType.VECTOR_STORE_ERROR
46
- assert "Error insertando documentos en PostgreSQL" in str(excinfo.value)
47
- mock_session.rollback.assert_called_once()
48
-
49
- def test_add_document_success(self, vs_repo, mock_db_manager, mock_embedder):
50
- mock_session = mock_db_manager.get_session.return_value
51
-
52
- vs_chunk_list = [
53
- VSDoc(id=1, text="Documento de prueba 1"),
54
- VSDoc(id=2, text="Documento de prueba 2")
55
- ]
56
-
57
- vs_repo.add_document(vs_chunk_list)
58
-
59
- # Verificar que las embeddings se generaron y los documentos fueron añadidos
60
- assert mock_embedder.feature_extraction.call_count == len(vs_chunk_list)
61
- assert mock_session.add.call_count == len(vs_chunk_list)
62
- mock_session.commit.assert_called_once()
63
-
64
-
65
- def test_query_raises_exception_on_error(self, vs_repo, mock_db_manager, mock_embedder):
66
- mock_session = mock_db_manager.get_session.return_value
67
- mock_session.execute.side_effect = Exception("Error en la base de datos")
68
-
69
- with pytest.raises(IAToolkitException) as excinfo:
70
- vs_repo.query(company_id=123, query_text="texto de prueba", n_results=3)
71
-
72
- assert excinfo.value.error_type == IAToolkitException.ErrorType.VECTOR_STORE_ERROR
73
- assert "Error en la consulta" in str(excinfo.value)
74
-
75
- def test_query_success(self, vs_repo, mock_db_manager, mock_embedder):
76
- mock_session = mock_db_manager.get_session.return_value
77
-
78
- # Simular resultados de la consulta en la base de datos
79
- mock_session.execute.return_value.fetchall.return_value = [
80
- (1, "filename1.txt", "contenido1", 'conb64'),
81
- (2, "filename2.txt", "contenido2", 'conb64')
82
- ]
83
-
84
- result = vs_repo.query(company_id=123, query_text="prompt_llm.txt de prueba", n_results=2)
85
-
86
- # Verificar resultados
87
- assert len(result) == 2
88
- assert result[0].id == 1
89
- assert result[0].filename == "filename1.txt"
90
- assert result[0].content == "contenido1"
91
- mock_embedder.feature_extraction.assert_called_once_with(["prompt_llm.txt de prueba"])
92
-
93
-
94
- def test_remove_duplicates_by_id(self, vs_repo):
95
- """Prueba para verificar la eliminación de duplicados por ID."""
96
- documents = [
97
- Document(id=1, company_id=123, filename="doc1.txt", content="contenido1", content_b64=''),
98
- Document(id=2, company_id=123, filename="doc2.txt", content="contenido2", content_b64=''),
99
- Document(id=1, company_id=123, filename="doc1.txt", content="contenido1", content_b64=''), # Duplicado
100
- ]
101
-
102
- result = vs_repo.remove_duplicates_by_id(documents)
103
-
104
- # Verificar que solo queden 2 documentos
105
- assert len(result) == 2
106
- assert result[0].id == 1
107
- assert result[1].id == 2
@@ -1,5 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
@@ -1,274 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
-
4
- import pytest
5
- from unittest.mock import MagicMock, patch
6
- from injector import Injector
7
- from iatoolkit.base_company import BaseCompany
8
- from iatoolkit.company_registry import get_company_registry, register_company
9
- from iatoolkit.services.dispatcher_service import Dispatcher
10
- from iatoolkit.common.exceptions import IAToolkitException
11
- from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
12
- from iatoolkit.repositories.profile_repo import ProfileRepo
13
- from iatoolkit.repositories.models import Company, Function
14
- from iatoolkit.services.excel_service import ExcelService
15
- from iatoolkit.services.prompt_manager_service import PromptService
16
- from iatoolkit.services.mail_service import MailService
17
- from iatoolkit.common.util import Utility
18
-
19
-
20
- # A mock company class for testing purposes
21
- class MockSampleCompany(BaseCompany):
22
- def register_company(self): pass
23
-
24
- def get_company_context(self, **kwargs) -> str: return "Company Context for Sample"
25
-
26
- def handle_request(self, tag: str, params: dict) -> dict: return {"result": "sample_company_response"}
27
-
28
- def start_execution(self): pass
29
-
30
- def get_user_info(self, user_identifier: str): pass
31
-
32
- def get_metadata_from_filename(self, filename: str) -> dict: return {}
33
-
34
-
35
- class TestDispatcher:
36
- @pytest.fixture(autouse=True)
37
- def setup(self):
38
- """Set up mocks, registry, and the Dispatcher for tests."""
39
- # Clean up the registry before each test to prevent interference
40
- registry = get_company_registry()
41
- registry.clear()
42
-
43
- # Mocks for services that are injected into the Dispatcher
44
- self.mock_prompt_manager = MagicMock()
45
- self.mock_llm_query_repo = MagicMock(spec=LLMQueryRepo)
46
- self.excel_service = MagicMock(spec=ExcelService)
47
- self.mail_service = MagicMock(spec=MailService)
48
- self.util = MagicMock(spec=Utility)
49
- self.mock_profile_repo = MagicMock(spec=ProfileRepo)
50
-
51
-
52
- # Create a mock injector that will be used for instantiation.
53
- mock_injector = Injector()
54
- mock_injector.binder.bind(ProfileRepo, to=self.mock_profile_repo)
55
- mock_injector.binder.bind(LLMQueryRepo, to=self.mock_llm_query_repo)
56
- mock_injector.binder.bind(PromptService, to=self.mock_prompt_manager)
57
-
58
- # Create a mock IAToolkit instance that returns our injector.
59
- self.toolkit_mock = MagicMock()
60
- self.toolkit_mock.get_injector.return_value = mock_injector
61
-
62
- # Patch IAToolkit.get_instance() to return our mock toolkit. This must be active
63
- # BEFORE any code that depends on the IAToolkit singleton is run.
64
- self.get_instance_patcher = patch('iatoolkit.iatoolkit.IAToolkit.get_instance',
65
- return_value=self.toolkit_mock)
66
- self.get_instance_patcher.start()
67
-
68
- # Now we can safely instantiate our mock company.
69
- self.mock_sample_company_instance = MockSampleCompany()
70
-
71
- # Mock methods that will be called
72
- self.mock_sample_company_instance.register_company = MagicMock()
73
- self.mock_sample_company_instance.handle_request = MagicMock(return_value={"result": "sample_company_response"})
74
- self.mock_sample_company_instance.get_company_context = MagicMock(return_value="Company Context for Sample")
75
- self.mock_sample_company_instance.start_execution = MagicMock(return_value=True)
76
- self.mock_sample_company_instance.get_user_info = MagicMock(return_value={"email": "test@user.com"})
77
- self.mock_sample_company_instance.get_metadata_from_filename = MagicMock(return_value={"meta": "data"})
78
-
79
- # Register the mock company class
80
- register_company("sample", MockSampleCompany)
81
-
82
- # Bind the mock instance in our injector. When the registry asks for an instance of
83
- # MockSampleCompany, the injector will return our pre-configured mock instance.
84
- mock_injector.binder.bind(MockSampleCompany, to=self.mock_sample_company_instance)
85
-
86
- # Instantiate all registered companies. The registry will use our mock_injector.
87
- registry.instantiate_companies(mock_injector)
88
-
89
- # Initialize the Dispatcher within the patched context
90
- self.dispatcher = Dispatcher(
91
- prompt_service=self.mock_prompt_manager,
92
- llmquery_repo=self.mock_llm_query_repo,
93
- util=self.util,
94
- excel_service=self.excel_service,
95
- mail_service=self.mail_service,
96
- )
97
-
98
-
99
- def teardown_method(self, method):
100
- """Clean up patches after each test."""
101
- if hasattr(self, 'get_instance_patcher'):
102
- self.get_instance_patcher.stop()
103
-
104
- # Clean up the registry
105
- registry = get_company_registry()
106
- registry.clear()
107
-
108
- def test_dispatch_sample_company(self):
109
- """Tests that dispatch works correctly for a valid company."""
110
- result = self.dispatcher.dispatch("sample", "some_data", key='a value')
111
- self.mock_sample_company_instance.handle_request.assert_called_once_with("some_data", key='a value')
112
- assert result == {"result": "sample_company_response"}
113
-
114
- def test_dispatch_invalid_company(self):
115
- """Tests that dispatch raises an exception for an unconfigured company."""
116
- with pytest.raises(IAToolkitException) as excinfo:
117
- self.dispatcher.dispatch("invalid_company", "some_tag")
118
- assert "Empresa 'invalid_company' no configurada" in str(excinfo.value)
119
-
120
- def test_dispatch_method_exception(self):
121
- """Validates that the dispatcher handles exceptions thrown by companies."""
122
- self.mock_sample_company_instance.handle_request.side_effect = Exception("Method error")
123
-
124
- with pytest.raises(IAToolkitException) as excinfo:
125
- self.dispatcher.dispatch("sample", "some_data")
126
-
127
- assert "Error en function call 'some_data'" in str(excinfo.value)
128
- assert "Method error" in str(excinfo.value)
129
-
130
- def test_dispatch_system_function(self):
131
- """Tests that dispatch correctly handles system functions."""
132
- self.excel_service.excel_generator.return_value = {"file": "test.xlsx"}
133
-
134
- result = self.dispatcher.dispatch("sample", "iat_generate_excel", filename="test.xlsx")
135
-
136
- self.excel_service.excel_generator.assert_called_once_with(filename="test.xlsx")
137
- self.mock_sample_company_instance.handle_request.assert_not_called()
138
- assert result == {"file": "test.xlsx"}
139
-
140
- def test_get_company_context(self):
141
- """Tests that get_company_context works correctly."""
142
- # Simulate no context files to simplify
143
- self.util.get_files_by_extension.return_value = []
144
-
145
- params = {"param1": "value1"}
146
- result = self.dispatcher.get_company_context("sample", **params)
147
-
148
- self.mock_sample_company_instance.get_company_context.assert_called_once_with(**params)
149
- assert "Company Context for Sample" in result
150
-
151
- def test_get_company_instance(self):
152
- """Tests that get_company_instance returns the correct company instance."""
153
- instance = self.dispatcher.get_company_instance("sample")
154
- assert instance == self.mock_sample_company_instance
155
-
156
- instance_none = self.dispatcher.get_company_instance("non_existent")
157
- assert instance_none is None
158
-
159
- def test_get_metadata_from_filename_success(self):
160
- """Tests that get_metadata_from_filename successfully calls the company's method."""
161
- filename = "test.txt"
162
- expected_metadata = {"meta": "data"}
163
- self.mock_sample_company_instance.get_metadata_from_filename.return_value = expected_metadata
164
-
165
- result = self.dispatcher.get_metadata_from_filename("sample", filename)
166
-
167
- self.mock_sample_company_instance.get_metadata_from_filename.assert_called_once_with(filename)
168
- assert result == expected_metadata
169
-
170
- def test_get_metadata_from_filename_invalid_company(self):
171
- """Tests get_metadata_from_filename with an invalid company."""
172
- with pytest.raises(IAToolkitException) as excinfo:
173
- self.dispatcher.get_metadata_from_filename("invalid_company", "test.txt")
174
- assert "Empresa no configurada: invalid_company" in str(excinfo.value)
175
-
176
- def test_get_metadata_from_filename_company_exception(self):
177
- """Tests get_metadata_from_filename when the company method raises an exception."""
178
- self.mock_sample_company_instance.get_metadata_from_filename.side_effect = Exception("Company error")
179
- with pytest.raises(IAToolkitException) as excinfo:
180
- self.dispatcher.get_metadata_from_filename("sample", "test.txt")
181
- assert "Error en get_metadata_from_filename de sample" in str(excinfo.value)
182
-
183
-
184
- def test_get_user_info_external_user(self):
185
- """Tests get_user_info for an external user."""
186
- user_identifier = "ext_user_123"
187
- expected_user_data = {"email": "external@example.com"}
188
- self.mock_sample_company_instance.get_user_info.return_value = expected_user_data
189
-
190
- result = self.dispatcher.get_user_info("sample", user_identifier, is_local_user=False)
191
-
192
- self.mock_sample_company_instance.get_user_info.assert_called_once_with(user_identifier)
193
- assert result["user_email"] == "external@example.com"
194
- assert not result["is_local"]
195
-
196
- def test_get_user_info_external_user_company_exception(self):
197
- """Tests get_user_info for an external user when the company method fails."""
198
- self.mock_sample_company_instance.get_user_info.side_effect = Exception("DB error")
199
- with pytest.raises(IAToolkitException) as excinfo:
200
- self.dispatcher.get_user_info("sample", "ext_user_123", is_local_user=False)
201
- assert "Error en get_user_info de sample" in str(excinfo.value)
202
-
203
- @patch('iatoolkit.services.dispatcher_service.SessionManager')
204
- def test_get_user_info_local_user(self, mock_session_manager):
205
- """Tests get_user_info for a local user from session."""
206
- user_identifier = "local_user_1"
207
- session_data = {"email": "local@iatoolkit.com", "user_fullname": "Local User"}
208
- mock_session_manager.get.return_value = session_data
209
-
210
- result = self.dispatcher.get_user_info("sample", user_identifier, is_local_user=True)
211
-
212
- mock_session_manager.get.assert_called_once_with('user', {})
213
- self.mock_sample_company_instance.get_user_info.assert_not_called()
214
- assert result["user_email"] == "local@iatoolkit.com"
215
- assert result["user_fullname"] == "Local User"
216
- assert result["is_local"]
217
-
218
- def test_get_user_info_invalid_company(self):
219
- """Tests get_user_info with an invalid company."""
220
- with pytest.raises(IAToolkitException) as excinfo:
221
- self.dispatcher.get_user_info("invalid_company", "any_user", is_local_user=False)
222
- assert "Empresa no configurada: invalid_company" in str(excinfo.value)
223
-
224
- def test_get_company_services(self):
225
- """Tests that get_company_services returns a correctly formatted list of tools."""
226
- # Mock Company and Function objects
227
- mock_company = MagicMock(spec=Company)
228
- mock_function = MagicMock(spec=Function)
229
- mock_function.name = "test_function"
230
- mock_function.description = "A test function"
231
- mock_function.parameters = {"type": "object", "properties": {}}
232
-
233
- self.mock_llm_query_repo.get_company_functions.return_value = [mock_function]
234
-
235
- tools = self.dispatcher.get_company_services(mock_company)
236
-
237
- self.mock_llm_query_repo.get_company_functions.assert_called_once_with(mock_company)
238
- assert len(tools) == 1
239
- tool = tools[0]
240
- assert tool["type"] == "function"
241
- assert tool["name"] == "test_function"
242
- assert tool["description"] == "A test function"
243
- assert tool["parameters"]["additionalProperties"] is False
244
- assert tool["strict"] is True
245
-
246
- def test_dispatcher_with_no_companies_registered(self):
247
- """Tests that the dispatcher works if no company is registered."""
248
- # Stop the current patch first
249
- self.get_instance_patcher.stop()
250
-
251
- # Clean registry
252
- get_company_registry().clear()
253
-
254
- toolkit_mock = MagicMock()
255
- toolkit_mock.get_injector.return_value = Injector() # Empty injector
256
-
257
- # Start a new patch for this specific test
258
- with patch('iatoolkit.iatoolkit.IAToolkit.get_instance', return_value=toolkit_mock):
259
- dispatcher = Dispatcher(
260
- prompt_service=self.mock_prompt_manager,
261
- llmquery_repo=self.mock_llm_query_repo,
262
- util=self.util,
263
- excel_service=self.excel_service,
264
- mail_service=self.mail_service,
265
- )
266
-
267
- assert len(dispatcher.company_instances) == 0
268
-
269
- with pytest.raises(IAToolkitException) as excinfo:
270
- dispatcher.dispatch("any_company", "some_action")
271
- assert "Empresa 'any_company' no configurada" in str(excinfo.value)
272
-
273
- # Restart the main patch for subsequent tests
274
- self.get_instance_patcher.start()
@@ -1,181 +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.services.document_service import DocumentService
9
- from iatoolkit.common.exceptions import IAToolkitException
10
-
11
-
12
- class TestDocumentService:
13
-
14
- @pytest.fixture(autouse=True)
15
- def setup_env_vars(self, monkeypatch):
16
- """
17
- Configura las variables de entorno necesarias para DocumentService
18
- antes de cada test.
19
- """
20
- monkeypatch.setenv("MAX_DOC_PAGES", "10")
21
-
22
- def test_initialization(self):
23
- """Prueba la inicialización de DocumentService con configuración correcta."""
24
- service = DocumentService()
25
- assert service.max_doc_pages == 10
26
-
27
- def test_file_txt_when_binary_content(self):
28
- service = DocumentService()
29
- result = service.file_to_txt("test.txt", b"dummy_content")
30
- assert result == "dummy_content"
31
-
32
- def test_file_txt_when_binary_content_and_error_decoding(self):
33
- service = DocumentService()
34
- with pytest.raises(IAToolkitException) as excinfo:
35
- result = service.file_to_txt("test.txt", b'\xff\xfe\xff'
36
- )
37
- assert "FILE_FORMAT_ERROR" == excinfo.value.error_type.name
38
-
39
- def test_file_txt_when_txt_content(self):
40
- service = DocumentService()
41
- result = service.file_to_txt("test.txt", "dummy_content")
42
- assert result == "dummy_content"
43
-
44
- @patch("iatoolkit.services.document_service.DocumentService.is_scanned_pdf")
45
- @patch("iatoolkit.services.document_service.DocumentService.read_scanned_pdf", return_value="Scanned text")
46
- @patch("iatoolkit.services.document_service.DocumentService.read_pdf", return_value="PDF text")
47
- def test_extension_file_detection(
48
- self, mock_read_pdf, mock_read_scanned_pdf, mock_is_scanned_pdf):
49
-
50
- mock_is_scanned_pdf.return_value = True
51
- service = DocumentService()
52
- result = service.file_to_txt("test.pdf", "dummy_content")
53
-
54
- assert result == "Scanned text"
55
-
56
- mock_is_scanned_pdf.return_value = False
57
- result = service.file_to_txt("test.pdf", "dummy_content")
58
- assert result == "PDF text"
59
-
60
-
61
- @patch("iatoolkit.services.document_service.io")
62
- @patch("iatoolkit.services.document_service.Document")
63
- def test_read_docx_successful(self, mock_document, mock_io):
64
- """Prueba que un archivo .docx se lea correctamente."""
65
- mock_io.BytesIO.return_value = b'a file'
66
- mock_doc = MagicMock()
67
- mock_doc.paragraphs = [
68
- MagicMock(text="First paragraph"),
69
- MagicMock(text="Second paragraph"),
70
- ]
71
- mock_document.return_value = mock_doc
72
-
73
- service = DocumentService()
74
- content = service.read_docx("dummy_docx_content")
75
- assert content == "# First paragraph\n\n# Second paragraph\n\n"
76
-
77
- @patch("iatoolkit.services.document_service.fitz.open")
78
- def test_read_pdf_successful(self, mock_fitz_open):
79
- """Prueba que un archivo PDF con prompt_llm.txt se lea correctamente."""
80
- mock_pdf = MagicMock()
81
- mock_pdf.__enter__.return_value = mock_pdf
82
- mock_pdf.__iter__.return_value = [
83
- MagicMock(get_text=MagicMock(return_value="Page 1 text")),
84
- MagicMock(get_text=MagicMock(return_value="Page 2 text")),
85
- ]
86
- mock_fitz_open.return_value = mock_pdf
87
-
88
- service = DocumentService()
89
- content = service.read_pdf("dummy_pdf_content")
90
- assert content == "Page 1 textPage 2 text"
91
-
92
- @patch("iatoolkit.services.document_service.pytesseract.image_to_string", return_value="Scanned text")
93
- @patch("iatoolkit.services.document_service.Image.frombytes")
94
- @patch("iatoolkit.services.document_service.fitz.Pixmap")
95
- @patch("iatoolkit.services.document_service.fitz.open")
96
- def test_read_scanned_pdf(self, mock_fitz_open, mock_pixmap, mock_frombytes, mock_tesseract):
97
- """Prueba que un PDF escaneado convierta imágenes en prompt_llm.txt correctamente."""
98
- mock_pdf = MagicMock()
99
- mock_pdf.page_count = 2
100
- mock_pdf.__len__.return_value = 1
101
- mock_fitz_open.return_value = mock_pdf
102
-
103
- mock_pdf.__iter__.return_value = [
104
- MagicMock(get_images=MagicMock(return_value=[(1,)]))
105
- ]
106
- mock_pdf.__getitem__.return_value = MagicMock(get_images=MagicMock(return_value=[(1,)]))
107
-
108
- mock_pixmap_obj = MagicMock()
109
- mock_pixmap.return_value = mock_pixmap_obj
110
-
111
- # Simular retorno de atributos del Pixmap
112
- mock_pixmap_obj.n = 3
113
- mock_pixmap_obj.width = 100
114
- mock_pixmap_obj.height = 100
115
- mock_pixmap_obj.samples = b"dummy_pixels"
116
-
117
- mock_fitz_open.return_value[0].get_images.return_value = [(1,)]
118
- mock_fitz_open.return_value[0].__getitem__ = MagicMock(return_value=mock_pixmap)
119
-
120
- service = DocumentService()
121
- content = service.read_scanned_pdf(b"dummy_scanned_pdf_content")
122
- assert content == "Scanned text"
123
-
124
- @patch("iatoolkit.services.document_service.fitz.open") # Parcheamos fitz.open
125
- def test_is_scanned_pdf_with_selectable_text(self, mock_fitz_open):
126
- """Prueba que un PDF con prompt_llm.txt seleccionable retorne False."""
127
- # Mock del documento PDF con prompt_llm.txt seleccionable
128
- mock_pdf = MagicMock()
129
- mock_pdf.__len__.return_value = 1 # Simula 1 página
130
- mock_page = MagicMock()
131
- mock_page.get_text.return_value = "This is some selectable text"
132
- mock_pdf.__getitem__.return_value = mock_page
133
- mock_fitz_open.return_value = mock_pdf
134
-
135
- service = DocumentService()
136
- result = service.is_scanned_pdf(b"dummy_pdf_content")
137
- assert result is False # Tiene prompt_llm.txt seleccionable, no es escaneado
138
-
139
- @patch("iatoolkit.services.document_service.fitz.open") # Parcheamos fitz.open
140
- def test_is_scanned_pdf_with_scanned_images(self, mock_fitz_open):
141
- """Prueba que un PDF con imágenes (escaneado) retorne True."""
142
- # Mock del documento PDF con imágenes
143
- mock_pdf = MagicMock()
144
- mock_pdf.__len__.return_value = 1 # Simula 1 página
145
- mock_page = MagicMock()
146
- mock_page.get_text.return_value = "" # No hay prompt_llm.txt
147
- mock_page.get_images.return_value = [(1,)] # Hay imágenes
148
- mock_pdf.__getitem__.return_value = mock_page
149
- mock_fitz_open.return_value = mock_pdf
150
-
151
- service = DocumentService()
152
- result = service.is_scanned_pdf(b"dummy_pdf_content")
153
- assert result is True # Escaneado, tiene imágenes pero no prompt_llm.txt
154
-
155
- @patch("iatoolkit.services.document_service.fitz.open") # Parcheamos fitz.open
156
- def test_is_scanned_pdf_without_text_or_images(self, mock_fitz_open):
157
- """Prueba que un PDF sin prompt_llm.txt y sin imágenes retorne True."""
158
- # Mock del documento PDF vacío (no tiene prompt_llm.txt ni imágenes)
159
- mock_pdf = MagicMock()
160
- mock_pdf.__len__.return_value = 1 # Simula 1 página
161
- mock_page = MagicMock()
162
- mock_page.get_text.return_value = "" # No hay prompt_llm.txt
163
- mock_page.get_images.return_value = [] # No hay imágenes
164
- mock_pdf.__getitem__.return_value = mock_page
165
- mock_fitz_open.return_value = mock_pdf
166
-
167
- service = DocumentService()
168
- result = service.is_scanned_pdf(b"dummy_pdf_content")
169
- assert result is True # No tiene prompt_llm.txt ni imágenes, probablemente escaneado
170
-
171
- @patch("iatoolkit.services.document_service.fitz.open") # Parcheamos fitz.open
172
- def test_is_scanned_pdf_with_empty_pdf(self, mock_fitz_open):
173
- """Prueba que un PDF vacío (sin páginas) retorne True."""
174
- # Mock del documento PDF vacío (sin páginas)
175
- mock_pdf = MagicMock()
176
- mock_pdf.__len__.return_value = 0 # Sin páginas
177
- mock_fitz_open.return_value = mock_pdf
178
-
179
- service = DocumentService()
180
- result = service.is_scanned_pdf(b"dummy_pdf_content")
181
- assert result is True # Al no tener páginas, se considera escaneado