pygeai 0.6.0b11__py3-none-any.whl → 0.6.0b13__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.
- pygeai/_docs/source/content/ai_lab/cli.rst +4 -4
- pygeai/_docs/source/content/ai_lab/models.rst +169 -35
- pygeai/_docs/source/content/ai_lab/runner.rst +2 -2
- pygeai/_docs/source/content/ai_lab/spec.rst +9 -9
- pygeai/_docs/source/content/ai_lab/usage.rst +34 -34
- pygeai/_docs/source/content/ai_lab.rst +1 -1
- pygeai/_docs/source/content/analytics.rst +598 -0
- pygeai/_docs/source/content/api_reference/chat.rst +428 -2
- pygeai/_docs/source/content/api_reference/embeddings.rst +1 -1
- pygeai/_docs/source/content/api_reference/project.rst +184 -0
- pygeai/_docs/source/content/api_reference/rag.rst +2 -2
- pygeai/_docs/source/content/authentication.rst +295 -0
- pygeai/_docs/source/content/cli.rst +79 -2
- pygeai/_docs/source/content/debugger.rst +1 -1
- pygeai/_docs/source/content/migration.rst +19 -2
- pygeai/_docs/source/index.rst +2 -0
- pygeai/_docs/source/pygeai.analytics.rst +53 -0
- pygeai/_docs/source/pygeai.cli.commands.rst +8 -0
- pygeai/_docs/source/pygeai.rst +1 -0
- pygeai/_docs/source/pygeai.tests.analytics.rst +45 -0
- pygeai/_docs/source/pygeai.tests.auth.rst +8 -0
- pygeai/_docs/source/pygeai.tests.rst +1 -1
- pygeai/analytics/__init__.py +0 -0
- pygeai/analytics/clients.py +505 -0
- pygeai/analytics/endpoints.py +35 -0
- pygeai/analytics/managers.py +606 -0
- pygeai/analytics/mappers.py +207 -0
- pygeai/analytics/responses.py +240 -0
- pygeai/assistant/managers.py +1 -1
- pygeai/chat/managers.py +1 -1
- pygeai/cli/commands/analytics.py +525 -0
- pygeai/cli/commands/base.py +16 -0
- pygeai/cli/commands/common.py +28 -24
- pygeai/cli/commands/migrate.py +75 -6
- pygeai/cli/commands/organization.py +265 -0
- pygeai/cli/commands/validators.py +144 -1
- pygeai/cli/error_handler.py +41 -6
- pygeai/cli/geai.py +106 -18
- pygeai/cli/parsers.py +75 -31
- pygeai/cli/texts/help.py +75 -6
- pygeai/core/base/clients.py +18 -4
- pygeai/core/base/session.py +59 -7
- pygeai/core/common/config.py +25 -2
- pygeai/core/common/exceptions.py +64 -1
- pygeai/core/embeddings/managers.py +1 -1
- pygeai/core/files/managers.py +1 -1
- pygeai/core/rerank/managers.py +1 -1
- pygeai/core/services/rest.py +20 -2
- pygeai/evaluation/clients.py +5 -3
- pygeai/lab/agents/clients.py +3 -3
- pygeai/lab/agents/endpoints.py +2 -2
- pygeai/lab/agents/mappers.py +50 -2
- pygeai/lab/clients.py +5 -2
- pygeai/lab/managers.py +8 -10
- pygeai/lab/models.py +70 -2
- pygeai/lab/tools/clients.py +1 -59
- pygeai/migration/__init__.py +3 -1
- pygeai/migration/strategies.py +72 -3
- pygeai/organization/clients.py +110 -1
- pygeai/organization/endpoints.py +11 -7
- pygeai/organization/limits/managers.py +1 -1
- pygeai/organization/managers.py +135 -3
- pygeai/organization/mappers.py +28 -2
- pygeai/organization/responses.py +11 -1
- pygeai/tests/analytics/__init__.py +0 -0
- pygeai/tests/analytics/test_clients.py +86 -0
- pygeai/tests/analytics/test_managers.py +94 -0
- pygeai/tests/analytics/test_mappers.py +84 -0
- pygeai/tests/analytics/test_responses.py +73 -0
- pygeai/tests/auth/test_oauth.py +172 -0
- pygeai/tests/cli/commands/test_migrate.py +14 -1
- pygeai/tests/cli/commands/test_organization.py +69 -1
- pygeai/tests/cli/test_error_handler.py +4 -4
- pygeai/tests/cli/test_geai_driver.py +1 -1
- pygeai/tests/lab/agents/test_mappers.py +128 -1
- pygeai/tests/lab/test_models.py +2 -0
- pygeai/tests/lab/tools/test_clients.py +2 -31
- pygeai/tests/organization/test_clients.py +180 -1
- pygeai/tests/organization/test_managers.py +40 -0
- pygeai/tests/snippets/analytics/__init__.py +0 -0
- pygeai/tests/snippets/analytics/get_agent_usage_per_user.py +16 -0
- pygeai/tests/snippets/analytics/get_agents_created_and_modified.py +11 -0
- pygeai/tests/snippets/analytics/get_average_cost_per_request.py +10 -0
- pygeai/tests/snippets/analytics/get_overall_error_rate.py +10 -0
- pygeai/tests/snippets/analytics/get_top_10_agents_by_requests.py +12 -0
- pygeai/tests/snippets/analytics/get_total_active_users.py +10 -0
- pygeai/tests/snippets/analytics/get_total_cost.py +10 -0
- pygeai/tests/snippets/analytics/get_total_requests_per_day.py +12 -0
- pygeai/tests/snippets/analytics/get_total_tokens.py +12 -0
- pygeai/tests/snippets/chat/get_response_complete_example.py +67 -0
- pygeai/tests/snippets/chat/get_response_with_instructions.py +19 -0
- pygeai/tests/snippets/chat/get_response_with_metadata.py +24 -0
- pygeai/tests/snippets/chat/get_response_with_parallel_tools.py +58 -0
- pygeai/tests/snippets/chat/get_response_with_reasoning.py +21 -0
- pygeai/tests/snippets/chat/get_response_with_store.py +38 -0
- pygeai/tests/snippets/chat/get_response_with_truncation.py +24 -0
- pygeai/tests/snippets/lab/agents/create_agent_with_permissions.py +39 -0
- pygeai/tests/snippets/lab/agents/create_agent_with_properties.py +46 -0
- pygeai/tests/snippets/lab/agents/get_agent_with_new_fields.py +62 -0
- pygeai/tests/snippets/lab/agents/update_agent_properties.py +50 -0
- pygeai/tests/snippets/organization/add_project_member.py +10 -0
- pygeai/tests/snippets/organization/add_project_member_batch.py +44 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b13.dist-info}/METADATA +1 -1
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b13.dist-info}/RECORD +108 -98
- pygeai/_docs/source/pygeai.tests.snippets.assistants.data_analyst.rst +0 -37
- pygeai/_docs/source/pygeai.tests.snippets.assistants.rag.rst +0 -85
- pygeai/_docs/source/pygeai.tests.snippets.assistants.rst +0 -78
- pygeai/_docs/source/pygeai.tests.snippets.auth.rst +0 -10
- pygeai/_docs/source/pygeai.tests.snippets.chat.rst +0 -125
- pygeai/_docs/source/pygeai.tests.snippets.dbg.rst +0 -45
- pygeai/_docs/source/pygeai.tests.snippets.embeddings.rst +0 -61
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.dataset.rst +0 -197
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.plan.rst +0 -133
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.result.rst +0 -37
- pygeai/_docs/source/pygeai.tests.snippets.evaluation.rst +0 -20
- pygeai/_docs/source/pygeai.tests.snippets.extras.rst +0 -37
- pygeai/_docs/source/pygeai.tests.snippets.files.rst +0 -53
- pygeai/_docs/source/pygeai.tests.snippets.gam.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.lab.agents.rst +0 -93
- pygeai/_docs/source/pygeai.tests.snippets.lab.processes.jobs.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.lab.processes.kbs.rst +0 -45
- pygeai/_docs/source/pygeai.tests.snippets.lab.processes.rst +0 -46
- pygeai/_docs/source/pygeai.tests.snippets.lab.rst +0 -82
- pygeai/_docs/source/pygeai.tests.snippets.lab.samples.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.lab.strategies.rst +0 -45
- pygeai/_docs/source/pygeai.tests.snippets.lab.tools.rst +0 -85
- pygeai/_docs/source/pygeai.tests.snippets.lab.use_cases.rst +0 -117
- pygeai/_docs/source/pygeai.tests.snippets.migrate.rst +0 -10
- pygeai/_docs/source/pygeai.tests.snippets.organization.rst +0 -109
- pygeai/_docs/source/pygeai.tests.snippets.rag.rst +0 -85
- pygeai/_docs/source/pygeai.tests.snippets.rerank.rst +0 -21
- pygeai/_docs/source/pygeai.tests.snippets.rst +0 -32
- pygeai/_docs/source/pygeai.tests.snippets.secrets.rst +0 -10
- pygeai/_docs/source/pygeai.tests.snippets.usage_limit.rst +0 -77
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b13.dist-info}/WHEEL +0 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b13.dist-info}/entry_points.txt +0 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b13.dist-info}/licenses/LICENSE +0 -0
- {pygeai-0.6.0b11.dist-info → pygeai-0.6.0b13.dist-info}/top_level.txt +0 -0
|
@@ -70,7 +70,7 @@ class TestErrorHandler(TestCase):
|
|
|
70
70
|
def test_format_error_basic(self):
|
|
71
71
|
"""Test basic error formatting"""
|
|
72
72
|
result = ErrorHandler.format_error("Test Error", "Something went wrong")
|
|
73
|
-
self.assertIn("ERROR: Something went wrong", result)
|
|
73
|
+
self.assertIn("ERROR [Test Error]: Something went wrong", result)
|
|
74
74
|
self.assertIn("Run 'geai help' for usage information.", result)
|
|
75
75
|
|
|
76
76
|
def test_format_error_with_suggestion(self):
|
|
@@ -80,7 +80,7 @@ class TestErrorHandler(TestCase):
|
|
|
80
80
|
"Command not found",
|
|
81
81
|
suggestion="Try using 'help' command"
|
|
82
82
|
)
|
|
83
|
-
self.assertIn("ERROR: Command not found", result)
|
|
83
|
+
self.assertIn("ERROR [Test Error]: Command not found", result)
|
|
84
84
|
self.assertIn("→ Try using 'help' command", result)
|
|
85
85
|
|
|
86
86
|
def test_format_error_without_help(self):
|
|
@@ -90,7 +90,7 @@ class TestErrorHandler(TestCase):
|
|
|
90
90
|
"Critical error",
|
|
91
91
|
show_help=False
|
|
92
92
|
)
|
|
93
|
-
self.assertIn("ERROR: Critical error", result)
|
|
93
|
+
self.assertIn("ERROR [Test Error]: Critical error", result)
|
|
94
94
|
self.assertNotIn("Run 'geai help'", result)
|
|
95
95
|
|
|
96
96
|
def test_find_similar_items_exact_match(self):
|
|
@@ -217,7 +217,7 @@ class TestErrorHandler(TestCase):
|
|
|
217
217
|
]
|
|
218
218
|
|
|
219
219
|
for result in results:
|
|
220
|
-
self.assertIn("ERROR
|
|
220
|
+
self.assertIn("ERROR [", result)
|
|
221
221
|
self.assertIn("→", result)
|
|
222
222
|
|
|
223
223
|
|
|
@@ -68,7 +68,7 @@ class TestCLIDriver(TestCase):
|
|
|
68
68
|
output = mock_stderr.getvalue()
|
|
69
69
|
|
|
70
70
|
# Check for standard error format
|
|
71
|
-
self.assertIn("ERROR
|
|
71
|
+
self.assertIn("ERROR [", output)
|
|
72
72
|
self.assertIn("→", output)
|
|
73
73
|
self.assertIn("Run 'geai help' for usage information", output)
|
|
74
74
|
|
|
@@ -310,4 +310,131 @@ class TestAgentMapper(TestCase):
|
|
|
310
310
|
sharing_link = AgentMapper.map_to_sharing_link(data)
|
|
311
311
|
self.assertEqual(sharing_link.agent_id, data["agentId"])
|
|
312
312
|
self.assertEqual(sharing_link.api_token, data["apiToken"])
|
|
313
|
-
self.assertEqual(sharing_link.shared_link, data["sharedLink"])
|
|
313
|
+
self.assertEqual(sharing_link.shared_link, data["sharedLink"])
|
|
314
|
+
|
|
315
|
+
def test_map_to_agent_with_new_fields(self):
|
|
316
|
+
"""Test mapping to Agent with new fields: sharing_scope, permissions, effective_permissions"""
|
|
317
|
+
data = {
|
|
318
|
+
"id": "agent123",
|
|
319
|
+
"name": "TestAgent",
|
|
320
|
+
"status": "active",
|
|
321
|
+
"sharingScope": "organization",
|
|
322
|
+
"permissions": {
|
|
323
|
+
"chatSharing": "organization",
|
|
324
|
+
"externalExecution": "none"
|
|
325
|
+
},
|
|
326
|
+
"effectivePermissions": {
|
|
327
|
+
"chatSharing": "organization",
|
|
328
|
+
"externalExecution": "none"
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
agent = AgentMapper.map_to_agent(data)
|
|
332
|
+
self.assertEqual(agent.id, "agent123")
|
|
333
|
+
self.assertEqual(agent.name, "TestAgent")
|
|
334
|
+
self.assertEqual(agent.sharing_scope, "organization")
|
|
335
|
+
self.assertIsNotNone(agent.permissions)
|
|
336
|
+
self.assertEqual(agent.permissions.chat_sharing, "organization")
|
|
337
|
+
self.assertEqual(agent.permissions.external_execution, "none")
|
|
338
|
+
self.assertIsNotNone(agent.effective_permissions)
|
|
339
|
+
self.assertEqual(agent.effective_permissions.chat_sharing, "organization")
|
|
340
|
+
self.assertEqual(agent.effective_permissions.external_execution, "none")
|
|
341
|
+
|
|
342
|
+
def test_map_to_agent_with_null_permissions(self):
|
|
343
|
+
"""Test mapping to Agent with null permissions and effective_permissions"""
|
|
344
|
+
data = {
|
|
345
|
+
"id": "agent123",
|
|
346
|
+
"name": "TestAgent",
|
|
347
|
+
"status": "active",
|
|
348
|
+
"sharingScope": "private"
|
|
349
|
+
}
|
|
350
|
+
agent = AgentMapper.map_to_agent(data)
|
|
351
|
+
self.assertEqual(agent.id, "agent123")
|
|
352
|
+
self.assertEqual(agent.sharing_scope, "private")
|
|
353
|
+
self.assertIsNone(agent.permissions)
|
|
354
|
+
self.assertIsNone(agent.effective_permissions)
|
|
355
|
+
|
|
356
|
+
def test_map_to_permission(self):
|
|
357
|
+
"""Test mapping to Permission"""
|
|
358
|
+
data = {
|
|
359
|
+
"chatSharing": "organization",
|
|
360
|
+
"externalExecution": "project"
|
|
361
|
+
}
|
|
362
|
+
permission = AgentMapper._map_to_permission(data)
|
|
363
|
+
self.assertEqual(permission.chat_sharing, "organization")
|
|
364
|
+
self.assertEqual(permission.external_execution, "project")
|
|
365
|
+
|
|
366
|
+
def test_map_to_permission_null(self):
|
|
367
|
+
"""Test mapping to Permission with None"""
|
|
368
|
+
permission = AgentMapper._map_to_permission(None)
|
|
369
|
+
self.assertIsNone(permission)
|
|
370
|
+
|
|
371
|
+
def test_map_to_agent_data_with_properties(self):
|
|
372
|
+
"""Test mapping to AgentData with properties field"""
|
|
373
|
+
data = {
|
|
374
|
+
"prompt": {"instructions": "Test"},
|
|
375
|
+
"llmConfig": {"maxTokens": 100},
|
|
376
|
+
"properties": [
|
|
377
|
+
{"dataType": "string", "key": "env", "value": "production"},
|
|
378
|
+
{"dataType": "number", "key": "max_retries", "value": "3"},
|
|
379
|
+
{"dataType": "boolean", "key": "enabled", "value": "true"}
|
|
380
|
+
],
|
|
381
|
+
"strategyName": "Dynamic Prompting"
|
|
382
|
+
}
|
|
383
|
+
agent_data = AgentMapper._map_agent_data(data)
|
|
384
|
+
self.assertIsNotNone(agent_data.properties)
|
|
385
|
+
self.assertEqual(len(agent_data.properties), 3)
|
|
386
|
+
self.assertEqual(agent_data.properties[0].data_type, "string")
|
|
387
|
+
self.assertEqual(agent_data.properties[0].key, "env")
|
|
388
|
+
self.assertEqual(agent_data.properties[0].value, "production")
|
|
389
|
+
self.assertEqual(agent_data.properties[1].data_type, "number")
|
|
390
|
+
self.assertEqual(agent_data.properties[1].key, "max_retries")
|
|
391
|
+
self.assertEqual(agent_data.properties[1].value, "3")
|
|
392
|
+
self.assertEqual(agent_data.properties[2].data_type, "boolean")
|
|
393
|
+
self.assertEqual(agent_data.properties[2].key, "enabled")
|
|
394
|
+
self.assertEqual(agent_data.properties[2].value, "true")
|
|
395
|
+
self.assertEqual(agent_data.strategy_name, "Dynamic Prompting")
|
|
396
|
+
|
|
397
|
+
def test_map_to_agent_data_with_null_properties(self):
|
|
398
|
+
"""Test mapping to AgentData with null properties"""
|
|
399
|
+
data = {
|
|
400
|
+
"prompt": {"instructions": "Test"},
|
|
401
|
+
"llmConfig": {"maxTokens": 100}
|
|
402
|
+
}
|
|
403
|
+
agent_data = AgentMapper._map_agent_data(data)
|
|
404
|
+
self.assertIsNone(agent_data.properties)
|
|
405
|
+
self.assertIsNone(agent_data.strategy_name)
|
|
406
|
+
|
|
407
|
+
def test_map_to_property(self):
|
|
408
|
+
"""Test mapping to Property"""
|
|
409
|
+
data = {
|
|
410
|
+
"dataType": "string",
|
|
411
|
+
"key": "environment",
|
|
412
|
+
"value": "production"
|
|
413
|
+
}
|
|
414
|
+
property_obj = AgentMapper._map_to_property(data)
|
|
415
|
+
self.assertEqual(property_obj.data_type, "string")
|
|
416
|
+
self.assertEqual(property_obj.key, "environment")
|
|
417
|
+
self.assertEqual(property_obj.value, "production")
|
|
418
|
+
|
|
419
|
+
def test_map_to_property_list(self):
|
|
420
|
+
"""Test mapping to Property list"""
|
|
421
|
+
data = [
|
|
422
|
+
{"dataType": "string", "key": "key1", "value": "value1"},
|
|
423
|
+
{"dataType": "number", "key": "key2", "value": "42"}
|
|
424
|
+
]
|
|
425
|
+
properties = AgentMapper._map_to_property_list(data)
|
|
426
|
+
self.assertEqual(len(properties), 2)
|
|
427
|
+
self.assertEqual(properties[0].key, "key1")
|
|
428
|
+
self.assertEqual(properties[0].value, "value1")
|
|
429
|
+
self.assertEqual(properties[1].key, "key2")
|
|
430
|
+
self.assertEqual(properties[1].value, "42")
|
|
431
|
+
|
|
432
|
+
def test_map_to_property_list_null(self):
|
|
433
|
+
"""Test mapping to Property list with None"""
|
|
434
|
+
properties = AgentMapper._map_to_property_list(None)
|
|
435
|
+
self.assertIsNone(properties)
|
|
436
|
+
|
|
437
|
+
def test_map_to_property_list_empty(self):
|
|
438
|
+
"""Test mapping to Property list with empty list"""
|
|
439
|
+
properties = AgentMapper._map_to_property_list([])
|
|
440
|
+
self.assertEqual(len(properties), 0)
|
pygeai/tests/lab/test_models.py
CHANGED
|
@@ -123,6 +123,7 @@ class TestLabModels(TestCase):
|
|
|
123
123
|
agent_data_data = {
|
|
124
124
|
"prompt": {"instructions": "Summarize", "inputs": ["text"], "outputs": [{"key": "summary", "description": "Summary"}]},
|
|
125
125
|
"llmConfig": {"maxTokens": 1000, "timeout": 30, "sampling": {"temperature": 0.7, "topK": 40, "topP": 0.9}},
|
|
126
|
+
"strategyName": "Dynamic Prompting",
|
|
126
127
|
"models": [{"name": "gpt-4"}]
|
|
127
128
|
}
|
|
128
129
|
agent_data = AgentData.model_validate(agent_data_data)
|
|
@@ -143,6 +144,7 @@ class TestLabModels(TestCase):
|
|
|
143
144
|
"agentData": {
|
|
144
145
|
"prompt": {"instructions": "Summarize", "inputs": ["text"], "outputs": [{"key": "summary", "description": "Summary"}]},
|
|
145
146
|
"llmConfig": {"maxTokens": 1000, "timeout": 30, "sampling": {"temperature": 0.7, "topK": 40, "topP": 0.9}},
|
|
147
|
+
"strategyName": "Dynamic Prompting",
|
|
146
148
|
"models": [{"name": "gpt-4"}]
|
|
147
149
|
}
|
|
148
150
|
}
|
|
@@ -68,8 +68,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
68
68
|
self.assertEqual(data['reportEvents'], report_events)
|
|
69
69
|
self.assertEqual(data['parameters'], self.parameters)
|
|
70
70
|
self.assertIn("automaticPublish=true", call_args[1]['endpoint'])
|
|
71
|
-
headers = call_args[1]['headers']
|
|
72
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
73
71
|
|
|
74
72
|
def test_create_tool_invalid_scope(self):
|
|
75
73
|
with self.assertRaises(ValueError) as context:
|
|
@@ -130,7 +128,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
130
128
|
self.assertEqual(result, expected_response)
|
|
131
129
|
mock_get.assert_called_once_with(
|
|
132
130
|
endpoint=LIST_TOOLS_V2,
|
|
133
|
-
headers=mock_get.call_args[1]['headers'],
|
|
134
131
|
params={
|
|
135
132
|
"id": "tool-1",
|
|
136
133
|
"count": "50",
|
|
@@ -140,8 +137,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
140
137
|
"allowExternal": True
|
|
141
138
|
}
|
|
142
139
|
)
|
|
143
|
-
headers = mock_get.call_args[1]['headers']
|
|
144
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
145
140
|
|
|
146
141
|
def test_list_tools_invalid_scope(self):
|
|
147
142
|
with self.assertRaises(ValueError) as context:
|
|
@@ -182,15 +177,12 @@ class TestToolClient(unittest.TestCase):
|
|
|
182
177
|
self.assertEqual(result, expected_response)
|
|
183
178
|
mock_get.assert_called_once_with(
|
|
184
179
|
endpoint=GET_TOOL_V2.format(toolId=self.tool_id),
|
|
185
|
-
headers=mock_get.call_args[1]['headers'],
|
|
186
180
|
params={
|
|
187
181
|
"revision": "1",
|
|
188
182
|
"version": 0,
|
|
189
183
|
"allowDrafts": True
|
|
190
184
|
}
|
|
191
185
|
)
|
|
192
|
-
headers = mock_get.call_args[1]['headers']
|
|
193
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
194
186
|
|
|
195
187
|
@patch("pygeai.core.services.rest.ApiService.get")
|
|
196
188
|
def test_get_tool_json_decode_error(self, mock_get):
|
|
@@ -218,11 +210,8 @@ class TestToolClient(unittest.TestCase):
|
|
|
218
210
|
|
|
219
211
|
self.assertEqual(result, {})
|
|
220
212
|
mock_delete.assert_called_once_with(
|
|
221
|
-
endpoint=DELETE_TOOL_V2.format(toolId=self.tool_id)
|
|
222
|
-
headers=mock_delete.call_args[1]['headers']
|
|
213
|
+
endpoint=DELETE_TOOL_V2.format(toolId=self.tool_id)
|
|
223
214
|
)
|
|
224
|
-
headers = mock_delete.call_args[1]['headers']
|
|
225
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
226
215
|
|
|
227
216
|
@patch("pygeai.core.services.rest.ApiService.delete")
|
|
228
217
|
def test_delete_tool_success_with_name(self, mock_delete):
|
|
@@ -235,11 +224,8 @@ class TestToolClient(unittest.TestCase):
|
|
|
235
224
|
|
|
236
225
|
self.assertEqual(result, {})
|
|
237
226
|
mock_delete.assert_called_once_with(
|
|
238
|
-
endpoint=DELETE_TOOL_V2.format(toolId=self.tool_name)
|
|
239
|
-
headers=mock_delete.call_args[1]['headers']
|
|
227
|
+
endpoint=DELETE_TOOL_V2.format(toolId=self.tool_name)
|
|
240
228
|
)
|
|
241
|
-
headers = mock_delete.call_args[1]['headers']
|
|
242
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
243
229
|
|
|
244
230
|
def test_delete_tool_invalid_input(self):
|
|
245
231
|
with self.assertRaises(ValueError) as context:
|
|
@@ -298,7 +284,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
298
284
|
self.assertEqual(result, expected_response)
|
|
299
285
|
mock_put.assert_called_once_with(
|
|
300
286
|
endpoint=f"{UPDATE_TOOL_V2.format(toolId=self.tool_id)}?automaticPublish=true",
|
|
301
|
-
headers=mock_put.call_args[1]['headers'],
|
|
302
287
|
data=mock_put.call_args[1]['data']
|
|
303
288
|
)
|
|
304
289
|
call_args = mock_put.call_args
|
|
@@ -313,8 +298,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
313
298
|
self.assertIsInstance(data['openApiJson'], str)
|
|
314
299
|
self.assertEqual(data['reportEvents'], report_events)
|
|
315
300
|
self.assertEqual(data['parameters'], self.parameters)
|
|
316
|
-
headers = call_args[1]['headers']
|
|
317
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
318
301
|
|
|
319
302
|
@patch("pygeai.core.services.rest.ApiService.put")
|
|
320
303
|
def test_update_tool_with_upsert(self, mock_put):
|
|
@@ -333,7 +316,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
333
316
|
self.assertEqual(result, expected_response)
|
|
334
317
|
mock_put.assert_called_once_with(
|
|
335
318
|
endpoint=UPSERT_TOOL_V2.format(toolId=self.tool_id),
|
|
336
|
-
headers=mock_put.call_args[1]['headers'],
|
|
337
319
|
data=mock_put.call_args[1]['data']
|
|
338
320
|
)
|
|
339
321
|
|
|
@@ -393,11 +375,8 @@ class TestToolClient(unittest.TestCase):
|
|
|
393
375
|
self.assertEqual(result, expected_response)
|
|
394
376
|
mock_post.assert_called_once_with(
|
|
395
377
|
endpoint=PUBLISH_TOOL_REVISION_V2.format(toolId=self.tool_id),
|
|
396
|
-
headers=mock_post.call_args[1]['headers'],
|
|
397
378
|
data={"revision": revision}
|
|
398
379
|
)
|
|
399
|
-
headers = mock_post.call_args[1]['headers']
|
|
400
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
401
380
|
|
|
402
381
|
@patch("pygeai.core.services.rest.ApiService.post")
|
|
403
382
|
def test_publish_tool_revision_json_decode_error(self, mock_post):
|
|
@@ -433,15 +412,12 @@ class TestToolClient(unittest.TestCase):
|
|
|
433
412
|
self.assertEqual(result, expected_response)
|
|
434
413
|
mock_get.assert_called_once_with(
|
|
435
414
|
endpoint=GET_PARAMETER_V2.format(toolPublicName=self.tool_id),
|
|
436
|
-
headers=mock_get.call_args[1]['headers'],
|
|
437
415
|
params={
|
|
438
416
|
"revision": "1",
|
|
439
417
|
"version": 0,
|
|
440
418
|
"allowDrafts": True
|
|
441
419
|
}
|
|
442
420
|
)
|
|
443
|
-
headers = mock_get.call_args[1]['headers']
|
|
444
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
445
421
|
|
|
446
422
|
@patch("pygeai.core.services.rest.ApiService.get")
|
|
447
423
|
def test_get_parameter_success_with_public_name(self, mock_get):
|
|
@@ -460,7 +436,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
460
436
|
self.assertEqual(result, expected_response)
|
|
461
437
|
mock_get.assert_called_once_with(
|
|
462
438
|
endpoint=GET_PARAMETER_V2.format(toolPublicName=self.tool_public_name),
|
|
463
|
-
headers=mock_get.call_args[1]['headers'],
|
|
464
439
|
params={
|
|
465
440
|
"revision": "1",
|
|
466
441
|
"version": 0,
|
|
@@ -501,11 +476,8 @@ class TestToolClient(unittest.TestCase):
|
|
|
501
476
|
self.assertEqual(result, {})
|
|
502
477
|
mock_post.assert_called_once_with(
|
|
503
478
|
endpoint=SET_PARAMETER_V2.format(toolPublicName=self.tool_id),
|
|
504
|
-
headers=mock_post.call_args[1]['headers'],
|
|
505
479
|
data={"parameterDefinition": {"parameters": self.parameters}}
|
|
506
480
|
)
|
|
507
|
-
headers = mock_post.call_args[1]['headers']
|
|
508
|
-
self.assertEqual(headers['ProjectId'], self.project_id)
|
|
509
481
|
|
|
510
482
|
@patch("pygeai.core.services.rest.ApiService.post")
|
|
511
483
|
def test_set_parameter_success_with_public_name(self, mock_post):
|
|
@@ -520,7 +492,6 @@ class TestToolClient(unittest.TestCase):
|
|
|
520
492
|
self.assertEqual(result, {})
|
|
521
493
|
mock_post.assert_called_once_with(
|
|
522
494
|
endpoint=SET_PARAMETER_V2.format(toolPublicName=self.tool_public_name),
|
|
523
|
-
headers=mock_post.call_args[1]['headers'],
|
|
524
495
|
data={"parameterDefinition": {"parameters": self.parameters}}
|
|
525
496
|
)
|
|
526
497
|
|
|
@@ -6,7 +6,8 @@ from pygeai.organization.clients import OrganizationClient
|
|
|
6
6
|
from pygeai.core.common.exceptions import InvalidAPIResponseException
|
|
7
7
|
from pygeai.organization.endpoints import GET_ASSISTANT_LIST_V1, GET_PROJECT_LIST_V1, GET_PROJECT_V1, CREATE_PROJECT_V1, \
|
|
8
8
|
UPDATE_PROJECT_V1, DELETE_PROJECT_V1, GET_PROJECT_TOKENS_V1, GET_REQUEST_DATA_V1, GET_MEMBERSHIPS_V2, \
|
|
9
|
-
GET_PROJECT_MEMBERSHIPS_V2, GET_PROJECT_ROLES_V2, GET_PROJECT_MEMBERS_V2, GET_ORGANIZATION_MEMBERS_V2
|
|
9
|
+
GET_PROJECT_MEMBERSHIPS_V2, GET_PROJECT_ROLES_V2, GET_PROJECT_MEMBERS_V2, GET_ORGANIZATION_MEMBERS_V2, \
|
|
10
|
+
ADD_PROJECT_MEMBER_V2, CREATE_ORGANIZATION_V2, GET_ORGANIZATION_LIST_V2, DELETE_ORGANIZATION_V2
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TestOrganizationClient(unittest.TestCase):
|
|
@@ -498,3 +499,181 @@ class TestOrganizationClient(unittest.TestCase):
|
|
|
498
499
|
|
|
499
500
|
self.assertEqual(str(context.exception), "Unable to get organization members for organization 'org-123': Invalid JSON response")
|
|
500
501
|
|
|
502
|
+
@patch("pygeai.core.services.rest.ApiService.post")
|
|
503
|
+
def test_add_project_member_success(self, mock_post):
|
|
504
|
+
mock_response = mock_post.return_value
|
|
505
|
+
mock_response.json.return_value = {"status": "invitation sent"}
|
|
506
|
+
mock_response.status_code = 201
|
|
507
|
+
|
|
508
|
+
result = self.client.add_project_member(
|
|
509
|
+
project_id="proj-123",
|
|
510
|
+
user_email="newuser@example.com",
|
|
511
|
+
roles=["Project member", "Project administrator"]
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
mock_post.assert_called_once_with(
|
|
515
|
+
endpoint=ADD_PROJECT_MEMBER_V2,
|
|
516
|
+
data={
|
|
517
|
+
"userEmail": "newuser@example.com",
|
|
518
|
+
"roles": ["Project member", "Project administrator"]
|
|
519
|
+
},
|
|
520
|
+
headers={"project-id": "proj-123"}
|
|
521
|
+
)
|
|
522
|
+
self.assertIsNotNone(result)
|
|
523
|
+
self.assertEqual(result['status'], "invitation sent")
|
|
524
|
+
|
|
525
|
+
@patch("pygeai.core.services.rest.ApiService.post")
|
|
526
|
+
def test_add_project_member_with_role_guids(self, mock_post):
|
|
527
|
+
mock_response = mock_post.return_value
|
|
528
|
+
mock_response.json.return_value = {"status": "invitation sent"}
|
|
529
|
+
mock_response.status_code = 201
|
|
530
|
+
|
|
531
|
+
result = self.client.add_project_member(
|
|
532
|
+
project_id="proj-123",
|
|
533
|
+
user_email="newuser@example.com",
|
|
534
|
+
roles=["guid-1", "guid-2"]
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
mock_post.assert_called_once_with(
|
|
538
|
+
endpoint=ADD_PROJECT_MEMBER_V2,
|
|
539
|
+
data={
|
|
540
|
+
"userEmail": "newuser@example.com",
|
|
541
|
+
"roles": ["guid-1", "guid-2"]
|
|
542
|
+
},
|
|
543
|
+
headers={"project-id": "proj-123"}
|
|
544
|
+
)
|
|
545
|
+
self.assertIsNotNone(result)
|
|
546
|
+
|
|
547
|
+
@patch("pygeai.core.services.rest.ApiService.post")
|
|
548
|
+
def test_add_project_member_json_decode_error(self, mock_post):
|
|
549
|
+
mock_response = mock_post.return_value
|
|
550
|
+
mock_response.status_code = 201
|
|
551
|
+
mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "", 0)
|
|
552
|
+
mock_response.text = "Invalid JSON response"
|
|
553
|
+
|
|
554
|
+
with self.assertRaises(InvalidAPIResponseException) as context:
|
|
555
|
+
self.client.add_project_member(
|
|
556
|
+
project_id="proj-123",
|
|
557
|
+
user_email="newuser@example.com",
|
|
558
|
+
roles=["Project member"]
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
self.assertEqual(str(context.exception), "Unable to add project member 'newuser@example.com': Invalid JSON response")
|
|
562
|
+
|
|
563
|
+
@patch("pygeai.core.services.rest.ApiService.post")
|
|
564
|
+
def test_create_organization_success(self, mock_post):
|
|
565
|
+
mock_response = mock_post.return_value
|
|
566
|
+
mock_response.json.return_value = {"organizationId": "org-123", "organizationName": "Test Org"}
|
|
567
|
+
mock_response.status_code = 201
|
|
568
|
+
|
|
569
|
+
result = self.client.create_organization(name="Test Org", administrator_user_email="admin@example.com")
|
|
570
|
+
|
|
571
|
+
mock_post.assert_called_once_with(
|
|
572
|
+
endpoint=CREATE_ORGANIZATION_V2,
|
|
573
|
+
data={
|
|
574
|
+
"name": "Test Org",
|
|
575
|
+
"administratorUserEmail": "admin@example.com"
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
self.assertIsNotNone(result)
|
|
579
|
+
self.assertEqual(result['organizationName'], "Test Org")
|
|
580
|
+
|
|
581
|
+
@patch("pygeai.core.services.rest.ApiService.post")
|
|
582
|
+
def test_create_organization_json_decode_error(self, mock_post):
|
|
583
|
+
mock_response = mock_post.return_value
|
|
584
|
+
mock_response.status_code = 201
|
|
585
|
+
mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "", 0)
|
|
586
|
+
mock_response.text = "Invalid JSON response"
|
|
587
|
+
|
|
588
|
+
with self.assertRaises(InvalidAPIResponseException) as context:
|
|
589
|
+
self.client.create_organization(name="Test Org", administrator_user_email="admin@example.com")
|
|
590
|
+
|
|
591
|
+
self.assertEqual(str(context.exception), "Unable to create organization with name 'Test Org': Invalid JSON response")
|
|
592
|
+
|
|
593
|
+
@patch("pygeai.core.services.rest.ApiService.get")
|
|
594
|
+
def test_get_organization_list_success(self, mock_get):
|
|
595
|
+
mock_response = mock_get.return_value
|
|
596
|
+
mock_response.json.return_value = {
|
|
597
|
+
"count": 2,
|
|
598
|
+
"pages": 1,
|
|
599
|
+
"organizations": [
|
|
600
|
+
{"organizationId": "org-1", "organizationName": "Org 1"},
|
|
601
|
+
{"organizationId": "org-2", "organizationName": "Org 2"}
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
mock_response.status_code = 200
|
|
605
|
+
|
|
606
|
+
result = self.client.get_organization_list()
|
|
607
|
+
|
|
608
|
+
mock_get.assert_called_once_with(
|
|
609
|
+
endpoint=GET_ORGANIZATION_LIST_V2,
|
|
610
|
+
params={"orderDirection": "desc"}
|
|
611
|
+
)
|
|
612
|
+
self.assertIsNotNone(result)
|
|
613
|
+
self.assertEqual(result['count'], 2)
|
|
614
|
+
self.assertEqual(len(result['organizations']), 2)
|
|
615
|
+
|
|
616
|
+
@patch("pygeai.core.services.rest.ApiService.get")
|
|
617
|
+
def test_get_organization_list_with_filters(self, mock_get):
|
|
618
|
+
mock_response = mock_get.return_value
|
|
619
|
+
mock_response.json.return_value = {"count": 1, "pages": 1, "organizations": [{"organizationId": "org-1", "organizationName": "Test"}]}
|
|
620
|
+
mock_response.status_code = 200
|
|
621
|
+
|
|
622
|
+
result = self.client.get_organization_list(
|
|
623
|
+
start_page=1,
|
|
624
|
+
page_size=10,
|
|
625
|
+
order_key="name",
|
|
626
|
+
order_direction="asc",
|
|
627
|
+
filter_key="name",
|
|
628
|
+
filter_value="Test"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
mock_get.assert_called_once_with(
|
|
632
|
+
endpoint=GET_ORGANIZATION_LIST_V2,
|
|
633
|
+
params={
|
|
634
|
+
"startPage": 1,
|
|
635
|
+
"pageSize": 10,
|
|
636
|
+
"orderKey": "name",
|
|
637
|
+
"orderDirection": "asc",
|
|
638
|
+
"filterKey": "name",
|
|
639
|
+
"filterValue": "Test"
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
self.assertIsNotNone(result)
|
|
643
|
+
|
|
644
|
+
@patch("pygeai.core.services.rest.ApiService.get")
|
|
645
|
+
def test_get_organization_list_json_decode_error(self, mock_get):
|
|
646
|
+
mock_response = mock_get.return_value
|
|
647
|
+
mock_response.status_code = 200
|
|
648
|
+
mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "", 0)
|
|
649
|
+
mock_response.text = "Invalid JSON response"
|
|
650
|
+
|
|
651
|
+
with self.assertRaises(InvalidAPIResponseException) as context:
|
|
652
|
+
self.client.get_organization_list()
|
|
653
|
+
|
|
654
|
+
self.assertEqual(str(context.exception), "Unable to get organization list: Invalid JSON response")
|
|
655
|
+
|
|
656
|
+
@patch("pygeai.core.services.rest.ApiService.delete")
|
|
657
|
+
def test_delete_organization_success(self, mock_delete):
|
|
658
|
+
mock_response = mock_delete.return_value
|
|
659
|
+
mock_response.json.return_value = {"status": "deleted"}
|
|
660
|
+
mock_response.status_code = 200
|
|
661
|
+
|
|
662
|
+
result = self.client.delete_organization(organization_id="org-123")
|
|
663
|
+
|
|
664
|
+
mock_delete.assert_called_once_with(endpoint=DELETE_ORGANIZATION_V2.format(organizationId="org-123"))
|
|
665
|
+
self.assertIsNotNone(result)
|
|
666
|
+
self.assertEqual(result['status'], "deleted")
|
|
667
|
+
|
|
668
|
+
@patch("pygeai.core.services.rest.ApiService.delete")
|
|
669
|
+
def test_delete_organization_json_decode_error(self, mock_delete):
|
|
670
|
+
mock_response = mock_delete.return_value
|
|
671
|
+
mock_response.status_code = 200
|
|
672
|
+
mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "", 0)
|
|
673
|
+
mock_response.text = "Invalid JSON response"
|
|
674
|
+
|
|
675
|
+
with self.assertRaises(InvalidAPIResponseException) as context:
|
|
676
|
+
self.client.delete_organization(organization_id="org-123")
|
|
677
|
+
|
|
678
|
+
self.assertEqual(str(context.exception), "Unable to delete organization with ID 'org-123': Invalid JSON response")
|
|
679
|
+
|
|
@@ -382,3 +382,43 @@ class TestOrganizationManager(unittest.TestCase):
|
|
|
382
382
|
|
|
383
383
|
self.assertIn("Error received while retrieving organization members", str(context.exception))
|
|
384
384
|
mock_get_organization_members.assert_called_once_with(organization_id="org-123")
|
|
385
|
+
|
|
386
|
+
@patch("pygeai.organization.clients.OrganizationClient.add_project_member")
|
|
387
|
+
def test_add_project_member(self, mock_add_project_member):
|
|
388
|
+
mock_response = EmptyResponse(content="Invitation sent successfully")
|
|
389
|
+
mock_add_project_member.return_value = {}
|
|
390
|
+
|
|
391
|
+
with patch.object(ResponseMapper, 'map_to_empty_response', return_value=mock_response):
|
|
392
|
+
response = self.manager.add_project_member(
|
|
393
|
+
project_id="proj-123",
|
|
394
|
+
user_email="newuser@example.com",
|
|
395
|
+
roles=["Project member"]
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
self.assertIsInstance(response, EmptyResponse)
|
|
399
|
+
self.assertEqual(response.content, "Invitation sent successfully")
|
|
400
|
+
mock_add_project_member.assert_called_once_with(
|
|
401
|
+
project_id="proj-123",
|
|
402
|
+
user_email="newuser@example.com",
|
|
403
|
+
roles=["Project member"]
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
@patch("pygeai.organization.clients.OrganizationClient.add_project_member")
|
|
407
|
+
def test_add_project_member_error(self, mock_add_project_member):
|
|
408
|
+
mock_add_project_member.return_value = self.error_response
|
|
409
|
+
|
|
410
|
+
with patch.object(ErrorHandler, 'has_errors', return_value=True):
|
|
411
|
+
with patch.object(ErrorHandler, 'extract_error', return_value="Invalid role"):
|
|
412
|
+
with self.assertRaises(APIError) as context:
|
|
413
|
+
self.manager.add_project_member(
|
|
414
|
+
project_id="proj-123",
|
|
415
|
+
user_email="newuser@example.com",
|
|
416
|
+
roles=["Invalid role"]
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
self.assertIn("Error received while adding project member", str(context.exception))
|
|
420
|
+
mock_add_project_member.assert_called_once_with(
|
|
421
|
+
project_id="proj-123",
|
|
422
|
+
user_email="newuser@example.com",
|
|
423
|
+
roles=["Invalid role"]
|
|
424
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pygeai.analytics.managers import AnalyticsManager
|
|
2
|
+
|
|
3
|
+
manager = AnalyticsManager()
|
|
4
|
+
|
|
5
|
+
response = manager.get_agent_usage_per_user(
|
|
6
|
+
start_date="2025-01-01",
|
|
7
|
+
end_date="2026-01-31"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
print("Agent usage per user:")
|
|
11
|
+
for user in response.agentUsagePerUser:
|
|
12
|
+
print(f"User: {user.userName or user.userId}")
|
|
13
|
+
print(f" Requests: {user.totalRequests}")
|
|
14
|
+
print(f" Tokens: {user.totalTokens}")
|
|
15
|
+
print(f" Cost: ${user.totalCost:.2f}")
|
|
16
|
+
print()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pygeai.analytics.managers import AnalyticsManager
|
|
2
|
+
|
|
3
|
+
manager = AnalyticsManager()
|
|
4
|
+
|
|
5
|
+
response = manager.get_agents_created_and_modified(
|
|
6
|
+
start_date="2025-01-01",
|
|
7
|
+
end_date="2026-01-31"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
print(f"Created agents: {response.createdAgents}")
|
|
11
|
+
print(f"Modified agents: {response.modifiedAgents}")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from pygeai.analytics.managers import AnalyticsManager
|
|
2
|
+
|
|
3
|
+
manager = AnalyticsManager()
|
|
4
|
+
|
|
5
|
+
response = manager.get_average_cost_per_request(
|
|
6
|
+
start_date="2025-01-01",
|
|
7
|
+
end_date="2026-01-31"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
print(f"Average cost per request: ${response.averageCost:.4f}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pygeai.analytics.managers import AnalyticsManager
|
|
2
|
+
|
|
3
|
+
manager = AnalyticsManager()
|
|
4
|
+
|
|
5
|
+
response = manager.get_top_10_agents_by_requests(
|
|
6
|
+
start_date="2025-01-01",
|
|
7
|
+
end_date="2026-01-31"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
print("Top 10 agents by requests:")
|
|
11
|
+
for idx, agent in enumerate(response.topAgents, 1):
|
|
12
|
+
print(f"{idx}. {agent.agentName}: {agent.totalRequests} requests")
|