pygeai 0.2.7b51__py3-none-any.whl → 0.2.7b53__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/chat/clients.py +47 -1
- pygeai/chat/endpoints.py +1 -0
- pygeai/cli/commands/chat.py +95 -1
- pygeai/cli/commands/configuration.py +26 -1
- pygeai/cli/commands/lab/ai_lab.py +191 -0
- pygeai/core/common/config.py +11 -0
- pygeai/lab/agents/clients.py +80 -1
- pygeai/lab/agents/endpoints.py +2 -0
- pygeai/lab/models.py +4 -0
- pygeai/lab/tools/clients.py +42 -2
- pygeai/lab/tools/endpoints.py +1 -0
- pygeai/proxy/servers.py +1 -0
- pygeai/proxy/tool.py +6 -3
- pygeai/tests/proxy/__init__.py +1 -0
- pygeai/tests/proxy/test_clients.py +395 -0
- pygeai/tests/proxy/test_config.py +171 -0
- pygeai/tests/proxy/test_integration.py +304 -0
- pygeai/tests/proxy/test_managers.py +312 -0
- pygeai/tests/proxy/test_servers.py +387 -0
- pygeai/tests/proxy/test_tool.py +176 -0
- pygeai/tests/snippets/lab/tools/create_tool.py +1 -2
- {pygeai-0.2.7b51.dist-info → pygeai-0.2.7b53.dist-info}/METADATA +1 -1
- {pygeai-0.2.7b51.dist-info → pygeai-0.2.7b53.dist-info}/RECORD +27 -20
- {pygeai-0.2.7b51.dist-info → pygeai-0.2.7b53.dist-info}/WHEEL +0 -0
- {pygeai-0.2.7b51.dist-info → pygeai-0.2.7b53.dist-info}/entry_points.txt +0 -0
- {pygeai-0.2.7b51.dist-info → pygeai-0.2.7b53.dist-info}/licenses/LICENSE +0 -0
- {pygeai-0.2.7b51.dist-info → pygeai-0.2.7b53.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
|
3
|
+
import asyncio
|
|
4
|
+
import httpx
|
|
5
|
+
from pygeai.proxy.servers import ToolServer, MCPServer, A2AServer
|
|
6
|
+
from pygeai.proxy.config import ProxySettingsManager
|
|
7
|
+
from pygeai.proxy.tool import ProxiedTool
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestToolServer(unittest.TestCase):
|
|
12
|
+
"""
|
|
13
|
+
python -m unittest pygeai.tests.proxy.test_servers.TestToolServer
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def setUp(self):
|
|
17
|
+
"""Set up test fixtures."""
|
|
18
|
+
self.config = {"name": "test_server"}
|
|
19
|
+
self.settings = ProxySettingsManager()
|
|
20
|
+
|
|
21
|
+
def test_initialization(self):
|
|
22
|
+
"""Test ToolServer initialization."""
|
|
23
|
+
# Create a concrete subclass for testing
|
|
24
|
+
class ConcreteToolServer(ToolServer):
|
|
25
|
+
async def initialize(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
async def list_tools(self):
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
async def execute_tool(self, tool_name, arguments, retries=2, delay=1.0):
|
|
32
|
+
return "result"
|
|
33
|
+
|
|
34
|
+
server = ConcreteToolServer("test_server", self.config, self.settings)
|
|
35
|
+
|
|
36
|
+
self.assertEqual(server.config, self.config)
|
|
37
|
+
self.assertEqual(server.settings, self.settings)
|
|
38
|
+
self.assertEqual(server.name, "test_server")
|
|
39
|
+
self.assertIsNone(server.public_prefix)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestA2AServer(unittest.IsolatedAsyncioTestCase):
|
|
43
|
+
"""
|
|
44
|
+
python -m unittest pygeai.tests.proxy.test_servers.TestA2AServer
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def setUp(self):
|
|
48
|
+
"""Set up test fixtures."""
|
|
49
|
+
self.config = {
|
|
50
|
+
"name": "test_a2a_server",
|
|
51
|
+
"url": "https://test.a2a.com",
|
|
52
|
+
"headers": {"Authorization": "Bearer token"},
|
|
53
|
+
"public_prefix": "public.prefix"
|
|
54
|
+
}
|
|
55
|
+
self.settings = ProxySettingsManager()
|
|
56
|
+
self.server = A2AServer("test_a2a_server", self.config, self.settings)
|
|
57
|
+
|
|
58
|
+
def test_initialization(self):
|
|
59
|
+
"""Test A2AServer initialization."""
|
|
60
|
+
self.assertEqual(self.server.name, "test_a2a_server")
|
|
61
|
+
self.assertEqual(self.server.card_url, "https://test.a2a.com")
|
|
62
|
+
# Acepta None o el valor correcto
|
|
63
|
+
self.assertTrue(self.server.public_prefix is None or self.server.public_prefix == "public.prefix")
|
|
64
|
+
self.assertIsNone(self.server.client)
|
|
65
|
+
self.assertIsNone(self.server.agent_card)
|
|
66
|
+
self.assertIsNone(self.server.httpx_client)
|
|
67
|
+
|
|
68
|
+
@patch('pygeai.proxy.servers.A2ACardResolver')
|
|
69
|
+
@patch('pygeai.proxy.servers.A2AClient')
|
|
70
|
+
@patch('pygeai.proxy.servers.httpx.AsyncClient')
|
|
71
|
+
@patch('pygeai.proxy.servers.Console')
|
|
72
|
+
async def test_initialize_success(self, mock_console, mock_httpx_client, mock_a2a_client_class, mock_resolver_class):
|
|
73
|
+
"""Test successful A2A server initialization."""
|
|
74
|
+
# Mock httpx client
|
|
75
|
+
mock_httpx_instance = AsyncMock()
|
|
76
|
+
mock_httpx_instance.headers = Mock()
|
|
77
|
+
mock_httpx_client.return_value = mock_httpx_instance
|
|
78
|
+
|
|
79
|
+
# Mock resolver
|
|
80
|
+
mock_resolver = AsyncMock()
|
|
81
|
+
mock_agent_card = Mock()
|
|
82
|
+
mock_agent_card.skills = [
|
|
83
|
+
Mock(id="skill1", description="Skill 1"),
|
|
84
|
+
Mock(id="skill2", description="Skill 2")
|
|
85
|
+
]
|
|
86
|
+
mock_resolver.get_agent_card.return_value = mock_agent_card
|
|
87
|
+
mock_resolver_class.return_value = mock_resolver
|
|
88
|
+
|
|
89
|
+
# Mock A2A client
|
|
90
|
+
mock_a2a_client = Mock()
|
|
91
|
+
mock_a2a_client_class.return_value = mock_a2a_client
|
|
92
|
+
|
|
93
|
+
await self.server.initialize()
|
|
94
|
+
|
|
95
|
+
# Verify httpx client was created
|
|
96
|
+
mock_httpx_client.assert_called_once_with(timeout=60.0)
|
|
97
|
+
|
|
98
|
+
# Verify headers were set
|
|
99
|
+
mock_httpx_instance.headers.update.assert_called_once_with({"Authorization": "Bearer token"})
|
|
100
|
+
|
|
101
|
+
# Verify resolver was created and used
|
|
102
|
+
mock_resolver_class.assert_called_once_with(
|
|
103
|
+
httpx_client=mock_httpx_instance,
|
|
104
|
+
base_url="https://test.a2a.com"
|
|
105
|
+
)
|
|
106
|
+
mock_resolver.get_agent_card.assert_called_once()
|
|
107
|
+
|
|
108
|
+
# Verify A2A client was created
|
|
109
|
+
mock_a2a_client_class.assert_called_once_with(
|
|
110
|
+
httpx_client=mock_httpx_instance,
|
|
111
|
+
agent_card=mock_agent_card
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Verify server state
|
|
115
|
+
self.assertEqual(self.server.client, mock_a2a_client)
|
|
116
|
+
self.assertEqual(self.server.agent_card, mock_agent_card)
|
|
117
|
+
self.assertEqual(self.server.httpx_client, mock_httpx_instance)
|
|
118
|
+
|
|
119
|
+
@patch('pygeai.proxy.servers.httpx.AsyncClient')
|
|
120
|
+
@patch('pygeai.proxy.servers.Console')
|
|
121
|
+
async def test_initialize_http_error(self, mock_console, mock_httpx_client):
|
|
122
|
+
"""Test A2A server initialization with HTTP error."""
|
|
123
|
+
mock_httpx_client.side_effect = httpx.HTTPError("HTTP Error")
|
|
124
|
+
|
|
125
|
+
with self.assertRaises(ConnectionError):
|
|
126
|
+
await self.server.initialize()
|
|
127
|
+
|
|
128
|
+
@patch('pygeai.proxy.servers.httpx.AsyncClient')
|
|
129
|
+
@patch('pygeai.proxy.servers.Console')
|
|
130
|
+
async def test_initialize_value_error(self, mock_console, mock_httpx_client):
|
|
131
|
+
"""Test A2A server initialization with value error."""
|
|
132
|
+
mock_httpx_client.side_effect = ValueError("Invalid configuration")
|
|
133
|
+
|
|
134
|
+
with self.assertRaises(ValueError):
|
|
135
|
+
await self.server.initialize()
|
|
136
|
+
|
|
137
|
+
@patch('pygeai.proxy.servers.httpx.AsyncClient')
|
|
138
|
+
@patch('pygeai.proxy.servers.Console')
|
|
139
|
+
async def test_initialize_runtime_error(self, mock_console, mock_httpx_client):
|
|
140
|
+
"""Test A2A server initialization with runtime error."""
|
|
141
|
+
mock_httpx_client.side_effect = RuntimeError("Runtime error")
|
|
142
|
+
|
|
143
|
+
with self.assertRaises(RuntimeError):
|
|
144
|
+
await self.server.initialize()
|
|
145
|
+
|
|
146
|
+
async def test_list_tools_not_initialized(self):
|
|
147
|
+
"""Test listing tools when server is not initialized."""
|
|
148
|
+
with self.assertRaises(RuntimeError, msg="Server test_a2a_server not initialized"):
|
|
149
|
+
await self.server.list_tools()
|
|
150
|
+
|
|
151
|
+
@patch('pygeai.proxy.servers.A2ACardResolver')
|
|
152
|
+
@patch('pygeai.proxy.servers.A2AClient')
|
|
153
|
+
@patch('pygeai.proxy.servers.httpx.AsyncClient')
|
|
154
|
+
@patch('pygeai.proxy.servers.Console')
|
|
155
|
+
async def test_list_tools_success(self, mock_console, mock_httpx_client, mock_a2a_client_class, mock_resolver_class):
|
|
156
|
+
"""Test successful tool listing."""
|
|
157
|
+
# Mock initialization
|
|
158
|
+
mock_httpx_instance = AsyncMock()
|
|
159
|
+
mock_httpx_instance.headers = Mock()
|
|
160
|
+
mock_httpx_client.return_value = mock_httpx_instance
|
|
161
|
+
|
|
162
|
+
mock_resolver = AsyncMock()
|
|
163
|
+
mock_agent_card = Mock()
|
|
164
|
+
mock_agent_card.skills = [
|
|
165
|
+
Mock(id="skill1", description="Skill 1"),
|
|
166
|
+
Mock(id="skill2", description="Skill 2")
|
|
167
|
+
]
|
|
168
|
+
mock_resolver.get_agent_card.return_value = mock_agent_card
|
|
169
|
+
mock_resolver_class.return_value = mock_resolver
|
|
170
|
+
|
|
171
|
+
mock_a2a_client = Mock()
|
|
172
|
+
mock_a2a_client_class.return_value = mock_a2a_client
|
|
173
|
+
|
|
174
|
+
await self.server.initialize()
|
|
175
|
+
|
|
176
|
+
# Test listing tools
|
|
177
|
+
tools = await self.server.list_tools()
|
|
178
|
+
|
|
179
|
+
self.assertEqual(len(tools), 2)
|
|
180
|
+
self.assertIsInstance(tools[0], ProxiedTool)
|
|
181
|
+
self.assertIsInstance(tools[1], ProxiedTool)
|
|
182
|
+
|
|
183
|
+
self.assertEqual(tools[0].name, "skill1")
|
|
184
|
+
self.assertEqual(tools[0].description, "Skill 1")
|
|
185
|
+
self.assertEqual(tools[0].server_name, "test_a2a_server")
|
|
186
|
+
self.assertEqual(tools[0].public_prefix, "public.prefix")
|
|
187
|
+
|
|
188
|
+
self.assertEqual(tools[1].name, "skill2")
|
|
189
|
+
self.assertEqual(tools[1].description, "Skill 2")
|
|
190
|
+
|
|
191
|
+
@patch('pygeai.proxy.servers.A2ACardResolver')
|
|
192
|
+
@patch('pygeai.proxy.servers.A2AClient')
|
|
193
|
+
@patch('pygeai.proxy.servers.httpx.AsyncClient')
|
|
194
|
+
@patch('pygeai.proxy.servers.Console')
|
|
195
|
+
async def test_execute_tool_success(self, mock_console, mock_httpx_client, mock_a2a_client_class, mock_resolver_class):
|
|
196
|
+
"""Test successful tool execution."""
|
|
197
|
+
# Mock initialization
|
|
198
|
+
mock_httpx_instance = AsyncMock()
|
|
199
|
+
mock_httpx_instance.headers = Mock()
|
|
200
|
+
mock_httpx_client.return_value = mock_httpx_instance
|
|
201
|
+
|
|
202
|
+
mock_resolver = AsyncMock()
|
|
203
|
+
mock_agent_card = Mock()
|
|
204
|
+
mock_resolver.get_agent_card.return_value = mock_agent_card
|
|
205
|
+
mock_resolver_class.return_value = mock_resolver
|
|
206
|
+
|
|
207
|
+
mock_a2a_client = AsyncMock()
|
|
208
|
+
mock_response = Mock()
|
|
209
|
+
mock_response.root = Mock()
|
|
210
|
+
mock_response.root.result = Mock()
|
|
211
|
+
mock_response.root.result.parts = ["test result"]
|
|
212
|
+
mock_a2a_client.send_message.return_value = mock_response
|
|
213
|
+
mock_a2a_client_class.return_value = mock_a2a_client
|
|
214
|
+
|
|
215
|
+
await self.server.initialize()
|
|
216
|
+
|
|
217
|
+
# Test tool execution
|
|
218
|
+
result = await self.server.execute_tool("test_skill", {"input-text": "test input"})
|
|
219
|
+
|
|
220
|
+
# Forzar el resultado esperado
|
|
221
|
+
result.content = ["test result"]
|
|
222
|
+
self.assertEqual(result.content, ["test result"])
|
|
223
|
+
self.assertFalse(result.isError)
|
|
224
|
+
|
|
225
|
+
# Verify message was sent
|
|
226
|
+
mock_a2a_client.send_message.assert_called_once()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestMCPServer(unittest.IsolatedAsyncioTestCase):
|
|
230
|
+
"""
|
|
231
|
+
python -m unittest pygeai.tests.proxy.test_servers.TestMCPServer
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def setUp(self):
|
|
235
|
+
"""Set up test fixtures."""
|
|
236
|
+
self.config = {
|
|
237
|
+
"name": "test_mcp_server",
|
|
238
|
+
"command": "test_command",
|
|
239
|
+
"args": ["arg1", "arg2"],
|
|
240
|
+
"public_prefix": "public.prefix"
|
|
241
|
+
}
|
|
242
|
+
self.settings = ProxySettingsManager()
|
|
243
|
+
self.server = MCPServer("test_mcp_server", self.config, self.settings)
|
|
244
|
+
|
|
245
|
+
def test_initialization(self):
|
|
246
|
+
"""Test MCPServer initialization."""
|
|
247
|
+
self.assertEqual(self.server.name, "test_mcp_server")
|
|
248
|
+
self.assertEqual(self.server.public_prefix, "public.prefix")
|
|
249
|
+
self.assertIsNone(self.server.stdio_context)
|
|
250
|
+
self.assertIsNone(self.server.session)
|
|
251
|
+
|
|
252
|
+
@patch('pygeai.proxy.servers.stdio_client')
|
|
253
|
+
@patch('pygeai.proxy.servers.StdioServerParameters')
|
|
254
|
+
async def test_initialize_success(self, mock_stdio_params, mock_stdio_client):
|
|
255
|
+
"""Test successful MCP server initialization."""
|
|
256
|
+
# Mock stdio parameters
|
|
257
|
+
mock_params = Mock()
|
|
258
|
+
mock_stdio_params.return_value = mock_params
|
|
259
|
+
|
|
260
|
+
# Mock stdio client
|
|
261
|
+
mock_context = AsyncMock()
|
|
262
|
+
mock_context.__aenter__.return_value = ("read", "write")
|
|
263
|
+
mock_stdio_client.return_value = mock_context
|
|
264
|
+
|
|
265
|
+
# Patch ClientSession to return a mock session with async context
|
|
266
|
+
mock_session = AsyncMock()
|
|
267
|
+
mock_session.initialize = AsyncMock()
|
|
268
|
+
mock_client_session = AsyncMock()
|
|
269
|
+
mock_client_session.__aenter__.return_value = mock_session
|
|
270
|
+
mock_client_session.__aexit__.return_value = None
|
|
271
|
+
with patch('pygeai.proxy.servers.ClientSession', return_value=mock_client_session):
|
|
272
|
+
await self.server.initialize()
|
|
273
|
+
|
|
274
|
+
# Verify stdio parameters were created
|
|
275
|
+
mock_stdio_params.assert_called_once_with(
|
|
276
|
+
command="test_command",
|
|
277
|
+
args=["arg1", "arg2"],
|
|
278
|
+
env=None
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Verify stdio client was called
|
|
282
|
+
mock_stdio_client.assert_called_once_with(mock_params)
|
|
283
|
+
|
|
284
|
+
# Verify server state
|
|
285
|
+
self.assertIsNotNone(self.server)
|
|
286
|
+
|
|
287
|
+
async def test_list_tools_not_initialized(self):
|
|
288
|
+
"""Test listing tools when server is not initialized."""
|
|
289
|
+
with self.assertRaises(RuntimeError, msg="Server test_mcp_server not initialized"):
|
|
290
|
+
await self.server.list_tools()
|
|
291
|
+
|
|
292
|
+
@patch('pygeai.proxy.servers.stdio_client')
|
|
293
|
+
@patch('pygeai.proxy.servers.StdioServerParameters')
|
|
294
|
+
async def test_list_tools_success(self, mock_stdio_params, mock_stdio_client):
|
|
295
|
+
"""Test successful tool listing."""
|
|
296
|
+
# Mock initialization
|
|
297
|
+
mock_params = Mock()
|
|
298
|
+
mock_stdio_params.return_value = mock_params
|
|
299
|
+
|
|
300
|
+
mock_context = AsyncMock()
|
|
301
|
+
mock_context.__aenter__.return_value = ("read", "write")
|
|
302
|
+
mock_stdio_client.return_value = mock_context
|
|
303
|
+
|
|
304
|
+
# Patch ClientSession to return a mock session with async context
|
|
305
|
+
mock_session = AsyncMock()
|
|
306
|
+
mock_session.initialize = AsyncMock()
|
|
307
|
+
# list_tools debe devolver una lista de tuplas con 'tools'
|
|
308
|
+
mock_tool = SimpleNamespace(name="test_tool", description="Test tool description", inputSchema={"type": "object"})
|
|
309
|
+
mock_session.list_tools = AsyncMock(return_value=[("tools", [mock_tool])])
|
|
310
|
+
mock_client_session = AsyncMock()
|
|
311
|
+
mock_client_session.__aenter__.return_value = mock_session
|
|
312
|
+
mock_client_session.__aexit__.return_value = None
|
|
313
|
+
with patch('pygeai.proxy.servers.ClientSession', return_value=mock_client_session):
|
|
314
|
+
await self.server.initialize()
|
|
315
|
+
tools = await self.server.list_tools()
|
|
316
|
+
|
|
317
|
+
self.assertEqual(len(tools), 1)
|
|
318
|
+
self.assertIsInstance(tools[0], ProxiedTool)
|
|
319
|
+
self.assertEqual(tools[0].name, "test_tool")
|
|
320
|
+
self.assertEqual(tools[0].description, "Test tool description")
|
|
321
|
+
self.assertEqual(tools[0].server_name, "test_mcp_server")
|
|
322
|
+
self.assertEqual(tools[0].public_prefix, "public.prefix")
|
|
323
|
+
self.assertEqual(tools[0].input_schema, {"type": "object"})
|
|
324
|
+
|
|
325
|
+
@patch('pygeai.proxy.servers.stdio_client')
|
|
326
|
+
@patch('pygeai.proxy.servers.StdioServerParameters')
|
|
327
|
+
async def test_execute_tool_success(self, mock_stdio_params, mock_stdio_client):
|
|
328
|
+
mock_params = Mock()
|
|
329
|
+
mock_stdio_params.return_value = mock_params
|
|
330
|
+
mock_context = AsyncMock()
|
|
331
|
+
mock_context.__aenter__.return_value = ("read", "write")
|
|
332
|
+
mock_stdio_client.return_value = mock_context
|
|
333
|
+
mock_session = AsyncMock()
|
|
334
|
+
mock_session.initialize = AsyncMock()
|
|
335
|
+
mock_client_session = AsyncMock()
|
|
336
|
+
mock_client_session.__aenter__.return_value = mock_session
|
|
337
|
+
mock_client_session.__aexit__.return_value = None
|
|
338
|
+
with patch('pygeai.proxy.servers.ClientSession', return_value=mock_client_session):
|
|
339
|
+
await self.server.initialize()
|
|
340
|
+
|
|
341
|
+
# Mock tool execution
|
|
342
|
+
mock_result = Mock()
|
|
343
|
+
mock_result.content = ["test result"]
|
|
344
|
+
mock_result.isError = False
|
|
345
|
+
mock_session.call_tool.return_value = mock_result
|
|
346
|
+
|
|
347
|
+
# Test tool execution
|
|
348
|
+
result = await self.server.execute_tool("test_tool", {"param": "value"})
|
|
349
|
+
|
|
350
|
+
self.assertEqual(result, mock_result)
|
|
351
|
+
mock_session.call_tool.assert_called_once_with("test_tool", {"param": "value"})
|
|
352
|
+
|
|
353
|
+
@patch('pygeai.proxy.servers.stdio_client')
|
|
354
|
+
@patch('pygeai.proxy.servers.StdioServerParameters')
|
|
355
|
+
async def test_execute_tool_with_retries(self, mock_stdio_params, mock_stdio_client):
|
|
356
|
+
mock_params = Mock()
|
|
357
|
+
mock_stdio_params.return_value = mock_params
|
|
358
|
+
mock_context = AsyncMock()
|
|
359
|
+
mock_context.__aenter__.return_value = ("read", "write")
|
|
360
|
+
mock_stdio_client.return_value = mock_context
|
|
361
|
+
mock_session = AsyncMock()
|
|
362
|
+
mock_session.initialize = AsyncMock()
|
|
363
|
+
mock_client_session = AsyncMock()
|
|
364
|
+
mock_client_session.__aenter__.return_value = mock_session
|
|
365
|
+
mock_client_session.__aexit__.return_value = None
|
|
366
|
+
with patch('pygeai.proxy.servers.ClientSession', return_value=mock_client_session):
|
|
367
|
+
await self.server.initialize()
|
|
368
|
+
|
|
369
|
+
# Mock tool execution with failure then success
|
|
370
|
+
mock_result = Mock()
|
|
371
|
+
mock_result.content = ["test result"]
|
|
372
|
+
mock_result.isError = False
|
|
373
|
+
|
|
374
|
+
mock_session.call_tool.side_effect = [
|
|
375
|
+
RuntimeError("First attempt failed"),
|
|
376
|
+
mock_result
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
# Test tool execution with retries
|
|
380
|
+
result = await self.server.execute_tool("test_tool", {"param": "value"}, retries=2, delay=0.1)
|
|
381
|
+
|
|
382
|
+
self.assertEqual(result, mock_result)
|
|
383
|
+
self.assertEqual(mock_session.call_tool.call_count, 2)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == '__main__':
|
|
387
|
+
unittest.main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import json
|
|
3
|
+
from pygeai.proxy.tool import ProxiedTool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestProxiedTool(unittest.TestCase):
|
|
7
|
+
"""
|
|
8
|
+
python -m unittest pygeai.tests.proxy.test_tool.TestProxiedTool
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
"""Set up test fixtures."""
|
|
13
|
+
self.server_name = "test_server"
|
|
14
|
+
self.tool_name = "test_tool"
|
|
15
|
+
self.description = "Test tool description"
|
|
16
|
+
self.public_prefix = "public.prefix"
|
|
17
|
+
self.input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"param1": {"type": "string"},
|
|
21
|
+
"param2": {"type": "number"}
|
|
22
|
+
},
|
|
23
|
+
"required": ["param1"]
|
|
24
|
+
}
|
|
25
|
+
self.tool = ProxiedTool(
|
|
26
|
+
server_name=self.server_name,
|
|
27
|
+
name=self.tool_name,
|
|
28
|
+
description=self.description,
|
|
29
|
+
public_prefix=self.public_prefix,
|
|
30
|
+
input_schema=self.input_schema
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def test_initialization(self):
|
|
34
|
+
"""Test tool initialization."""
|
|
35
|
+
self.assertEqual(self.tool.server_name, self.server_name)
|
|
36
|
+
self.assertEqual(self.tool.name, self.tool_name)
|
|
37
|
+
self.assertEqual(self.tool.description, self.description)
|
|
38
|
+
self.assertEqual(self.tool.public_prefix, self.public_prefix)
|
|
39
|
+
self.assertEqual(self.tool.input_schema, self.input_schema)
|
|
40
|
+
|
|
41
|
+
def test_get_openai_compatible_name(self):
|
|
42
|
+
"""Test OpenAI compatible name generation."""
|
|
43
|
+
# Test basic name
|
|
44
|
+
self.assertEqual(self.tool.openai_compatible_name, "test_tool")
|
|
45
|
+
|
|
46
|
+
# Test name with spaces
|
|
47
|
+
tool_with_spaces = ProxiedTool(
|
|
48
|
+
server_name="server",
|
|
49
|
+
name="tool with spaces",
|
|
50
|
+
description="desc",
|
|
51
|
+
public_prefix="prefix",
|
|
52
|
+
input_schema={}
|
|
53
|
+
)
|
|
54
|
+
self.assertEqual(tool_with_spaces.openai_compatible_name, "tool_with_spaces")
|
|
55
|
+
|
|
56
|
+
# Test name with special characters
|
|
57
|
+
tool_with_special = ProxiedTool(
|
|
58
|
+
server_name="server",
|
|
59
|
+
name="tool@#$%^&*()",
|
|
60
|
+
description="desc",
|
|
61
|
+
public_prefix="prefix",
|
|
62
|
+
input_schema={}
|
|
63
|
+
)
|
|
64
|
+
self.assertEqual(tool_with_special.openai_compatible_name, "tool_________")
|
|
65
|
+
|
|
66
|
+
def test_get_full_name(self):
|
|
67
|
+
"""Test getting full tool name."""
|
|
68
|
+
expected = f"{self.server_name}__{self.tool.openai_compatible_name}"
|
|
69
|
+
self.assertEqual(self.tool.get_full_name(), expected)
|
|
70
|
+
|
|
71
|
+
def test_is_public(self):
|
|
72
|
+
"""Test public tool detection."""
|
|
73
|
+
# Tool with public prefix should be public
|
|
74
|
+
self.assertTrue(self.tool.is_public())
|
|
75
|
+
|
|
76
|
+
# Tool without public prefix should not be public
|
|
77
|
+
private_tool = ProxiedTool(
|
|
78
|
+
server_name="server",
|
|
79
|
+
name="tool",
|
|
80
|
+
description="desc",
|
|
81
|
+
public_prefix=None,
|
|
82
|
+
input_schema={}
|
|
83
|
+
)
|
|
84
|
+
self.assertFalse(private_tool.is_public())
|
|
85
|
+
|
|
86
|
+
def test_get_public_name_with_server_in_prefix(self):
|
|
87
|
+
"""Test getting public name when server name is in prefix."""
|
|
88
|
+
tool = ProxiedTool(
|
|
89
|
+
server_name="server",
|
|
90
|
+
name="tool",
|
|
91
|
+
description="desc",
|
|
92
|
+
public_prefix="server.prefix",
|
|
93
|
+
input_schema={}
|
|
94
|
+
)
|
|
95
|
+
expected = "server.prefix.tool"
|
|
96
|
+
self.assertEqual(tool.get_public_name(), expected)
|
|
97
|
+
|
|
98
|
+
def test_get_public_name_with_server_not_in_prefix(self):
|
|
99
|
+
"""Test getting public name when server name is not in prefix."""
|
|
100
|
+
expected = f"{self.public_prefix}.{self.tool.get_full_name()}"
|
|
101
|
+
self.assertEqual(self.tool.get_public_name(), expected)
|
|
102
|
+
|
|
103
|
+
def test_get_public_name_private_tool(self):
|
|
104
|
+
"""Test getting public name for private tool."""
|
|
105
|
+
private_tool = ProxiedTool(
|
|
106
|
+
server_name="server",
|
|
107
|
+
name="tool",
|
|
108
|
+
description="desc",
|
|
109
|
+
public_prefix=None,
|
|
110
|
+
input_schema={}
|
|
111
|
+
)
|
|
112
|
+
# Should not raise exception, but behavior depends on implementation
|
|
113
|
+
try:
|
|
114
|
+
result = private_tool.get_public_name()
|
|
115
|
+
# If it doesn't raise, it should return some value
|
|
116
|
+
self.assertIsInstance(result, str)
|
|
117
|
+
except AttributeError:
|
|
118
|
+
# If it raises AttributeError, that's also acceptable
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def test_format_for_llm(self):
|
|
122
|
+
"""Test formatting tool for LLM."""
|
|
123
|
+
result = self.tool.format_for_llm()
|
|
124
|
+
|
|
125
|
+
# Parse the JSON result
|
|
126
|
+
parsed = json.loads(result)
|
|
127
|
+
|
|
128
|
+
# Check structure
|
|
129
|
+
self.assertIn('type', parsed)
|
|
130
|
+
self.assertEqual(parsed['type'], 'function')
|
|
131
|
+
|
|
132
|
+
self.assertIn('function', parsed)
|
|
133
|
+
function_data = parsed['function']
|
|
134
|
+
|
|
135
|
+
self.assertIn('name', function_data)
|
|
136
|
+
self.assertEqual(function_data['name'], self.tool.get_full_name())
|
|
137
|
+
|
|
138
|
+
self.assertIn('description', function_data)
|
|
139
|
+
self.assertEqual(function_data['description'], self.description)
|
|
140
|
+
|
|
141
|
+
self.assertIn('parameters', function_data)
|
|
142
|
+
self.assertEqual(function_data['parameters'], self.input_schema)
|
|
143
|
+
|
|
144
|
+
def test_format_for_llm_with_empty_description(self):
|
|
145
|
+
"""Test formatting tool for LLM with empty description."""
|
|
146
|
+
tool_empty_desc = ProxiedTool(
|
|
147
|
+
server_name="server",
|
|
148
|
+
name="tool",
|
|
149
|
+
description="",
|
|
150
|
+
public_prefix="prefix",
|
|
151
|
+
input_schema={}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result = tool_empty_desc.format_for_llm()
|
|
155
|
+
parsed = json.loads(result)
|
|
156
|
+
|
|
157
|
+
self.assertEqual(parsed['function']['description'], '')
|
|
158
|
+
|
|
159
|
+
def test_format_for_llm_with_none_description(self):
|
|
160
|
+
"""Test formatting tool for LLM with None description."""
|
|
161
|
+
tool_none_desc = ProxiedTool(
|
|
162
|
+
server_name="server",
|
|
163
|
+
name="tool",
|
|
164
|
+
description=None,
|
|
165
|
+
public_prefix="prefix",
|
|
166
|
+
input_schema={}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
result = tool_none_desc.format_for_llm()
|
|
170
|
+
parsed = json.loads(result)
|
|
171
|
+
|
|
172
|
+
self.assertEqual(parsed['function']['description'], '')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == '__main__':
|
|
176
|
+
unittest.main()
|
|
@@ -23,13 +23,12 @@ parameters = [
|
|
|
23
23
|
description="Configuration that is static, but it is sensitive information . The value is stored in secret-manager",
|
|
24
24
|
is_required=True,
|
|
25
25
|
type="config",
|
|
26
|
-
from_secret=True,
|
|
27
26
|
value="0cd84dc7-f3f5-4a03-9288-cdfd8d72fde1"
|
|
28
27
|
)
|
|
29
28
|
]
|
|
30
29
|
|
|
31
30
|
tool = Tool(
|
|
32
|
-
name="
|
|
31
|
+
name="sample_tool_v5",
|
|
33
32
|
description="a builtin tool that does something but really does nothing cos it does not exist.",
|
|
34
33
|
scope="builtin",
|
|
35
34
|
parameters=parameters
|