a2a-lite 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/PKG-INFO +3 -1
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/pyproject.toml +4 -1
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/__init__.py +1 -1
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/agent.py +29 -10
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/auth.py +2 -2
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/cli.py +1 -1
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/decorators.py +2 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/executor.py +23 -16
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/testing.py +10 -1
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/utils.py +5 -1
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/webhooks.py +2 -6
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_auth.py +86 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/.claude/settings.local.json +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/.gitignore +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/README.md +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/01_hello_world.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/02_calculator.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/03_async_agent.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/04_multi_agent/finance_agent.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/04_multi_agent/reporter_agent.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/04_multi_agent/run_demo.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/05_with_llm.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/06_pydantic_models.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/07_middleware.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/08_streaming.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/09_testing.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/10_webhooks.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/11_human_in_the_loop.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/12_file_handling.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/13_task_tracking.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/examples/14_with_auth.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/discovery.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/human_loop.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/middleware.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/parts.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/streaming.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/src/a2a_lite/tasks.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/__init__.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_agent.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_decorators.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_discovery.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_human_loop.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_integration.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_middleware.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_parts.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_pydantic.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_tasks.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_testing.py +0 -0
- {a2a_lite-0.2.1 → a2a_lite-0.2.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-lite
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Simplified wrapper for Google's A2A Protocol SDK
|
|
5
5
|
Author: A2A Lite Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -27,6 +27,8 @@ Provides-Extra: dev
|
|
|
27
27
|
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
28
28
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
29
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: oauth
|
|
31
|
+
Requires-Dist: pyjwt[crypto]>=2.0; extra == 'oauth'
|
|
30
32
|
Description-Content-Type: text/markdown
|
|
31
33
|
|
|
32
34
|
# A2A Lite - Python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-lite"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Simplified wrapper for Google's A2A Protocol SDK"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -32,6 +32,9 @@ dependencies = [
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
|
+
oauth = [
|
|
36
|
+
"pyjwt[crypto]>=2.0",
|
|
37
|
+
]
|
|
35
38
|
dev = [
|
|
36
39
|
"pytest>=7.0",
|
|
37
40
|
"pytest-asyncio>=0.21",
|
|
@@ -170,11 +170,14 @@ class Agent:
|
|
|
170
170
|
import typing
|
|
171
171
|
from .tasks import TaskContext as _TaskContext
|
|
172
172
|
from .human_loop import InteractionContext as _InteractionContext
|
|
173
|
+
from .auth import AuthResult as _AuthResult
|
|
173
174
|
|
|
174
175
|
needs_task_context = False
|
|
175
176
|
needs_interaction = False
|
|
177
|
+
needs_auth = False
|
|
176
178
|
task_context_param: str | None = None
|
|
177
179
|
interaction_param: str | None = None
|
|
180
|
+
auth_param: str | None = None
|
|
178
181
|
|
|
179
182
|
try:
|
|
180
183
|
resolved_hints = typing.get_type_hints(func)
|
|
@@ -190,6 +193,14 @@ class Agent:
|
|
|
190
193
|
elif _is_or_subclass(hint, _InteractionContext):
|
|
191
194
|
needs_interaction = True
|
|
192
195
|
interaction_param = param_name
|
|
196
|
+
elif _is_or_subclass(hint, _AuthResult):
|
|
197
|
+
needs_auth = True
|
|
198
|
+
auth_param = param_name
|
|
199
|
+
|
|
200
|
+
# Also detect require_auth decorator
|
|
201
|
+
if getattr(func, '__requires_auth__', False) and not needs_auth:
|
|
202
|
+
needs_auth = True
|
|
203
|
+
auth_param = auth_param or "auth"
|
|
193
204
|
|
|
194
205
|
# Extract schemas
|
|
195
206
|
input_schema, output_schema = extract_function_schemas(func)
|
|
@@ -205,8 +216,10 @@ class Agent:
|
|
|
205
216
|
is_streaming=is_streaming,
|
|
206
217
|
needs_task_context=needs_task_context,
|
|
207
218
|
needs_interaction=needs_interaction,
|
|
219
|
+
needs_auth=needs_auth,
|
|
208
220
|
task_context_param=task_context_param,
|
|
209
221
|
interaction_param=interaction_param,
|
|
222
|
+
auth_param=auth_param,
|
|
210
223
|
)
|
|
211
224
|
|
|
212
225
|
self._skills[skill_name] = skill_def
|
|
@@ -383,11 +396,14 @@ class Agent:
|
|
|
383
396
|
))
|
|
384
397
|
|
|
385
398
|
# Run startup hooks
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
asyncio.
|
|
389
|
-
|
|
390
|
-
|
|
399
|
+
async def _run_startup():
|
|
400
|
+
for hook in self._on_startup:
|
|
401
|
+
if asyncio.iscoroutinefunction(hook):
|
|
402
|
+
await hook()
|
|
403
|
+
else:
|
|
404
|
+
hook()
|
|
405
|
+
if self._on_startup:
|
|
406
|
+
asyncio.run(_run_startup())
|
|
391
407
|
|
|
392
408
|
# Enable discovery if requested
|
|
393
409
|
if enable_discovery:
|
|
@@ -434,11 +450,14 @@ class Agent:
|
|
|
434
450
|
)
|
|
435
451
|
finally:
|
|
436
452
|
# Run shutdown hooks
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
asyncio.
|
|
440
|
-
|
|
441
|
-
|
|
453
|
+
async def _run_shutdown():
|
|
454
|
+
for hook in self._on_shutdown:
|
|
455
|
+
if asyncio.iscoroutinefunction(hook):
|
|
456
|
+
await hook()
|
|
457
|
+
else:
|
|
458
|
+
hook()
|
|
459
|
+
if self._on_shutdown:
|
|
460
|
+
asyncio.run(_run_shutdown())
|
|
442
461
|
|
|
443
462
|
# Unregister discovery
|
|
444
463
|
if self._discovery:
|
|
@@ -222,7 +222,7 @@ class OAuth2Auth(AuthProvider):
|
|
|
222
222
|
audience="my-agent",
|
|
223
223
|
)
|
|
224
224
|
|
|
225
|
-
Requires: pip install
|
|
225
|
+
Requires: pip install a2a-lite[oauth]
|
|
226
226
|
"""
|
|
227
227
|
|
|
228
228
|
def __init__(
|
|
@@ -277,7 +277,7 @@ class OAuth2Auth(AuthProvider):
|
|
|
277
277
|
|
|
278
278
|
except ImportError:
|
|
279
279
|
return AuthResult.failure(
|
|
280
|
-
"OAuth2 requires pyjwt: pip install
|
|
280
|
+
"OAuth2 requires pyjwt: pip install a2a-lite[oauth]"
|
|
281
281
|
)
|
|
282
282
|
except Exception as e:
|
|
283
283
|
return AuthResult.failure(f"Token validation failed: {str(e)}")
|
|
@@ -18,8 +18,10 @@ class SkillDefinition:
|
|
|
18
18
|
is_streaming: bool = False
|
|
19
19
|
needs_task_context: bool = False
|
|
20
20
|
needs_interaction: bool = False
|
|
21
|
+
needs_auth: bool = False
|
|
21
22
|
task_context_param: Optional[str] = None
|
|
22
23
|
interaction_param: Optional[str] = None
|
|
24
|
+
auth_param: Optional[str] = None
|
|
23
25
|
|
|
24
26
|
def to_dict(self) -> Dict[str, Any]:
|
|
25
27
|
"""Convert to dictionary for serialization."""
|
|
@@ -59,21 +59,22 @@ class LiteAgentExecutor(AgentExecutor):
|
|
|
59
59
|
from a2a.utils import new_agent_text_message
|
|
60
60
|
|
|
61
61
|
try:
|
|
62
|
-
# Authenticate the request
|
|
62
|
+
# Authenticate the request (always run to produce auth_result for injection)
|
|
63
|
+
auth_result = None
|
|
63
64
|
if self.auth_provider:
|
|
64
65
|
from .auth import AuthRequest, NoAuth
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
headers = {}
|
|
67
|
+
if context.call_context and context.call_context.state:
|
|
68
|
+
headers = context.call_context.state.get('headers', {})
|
|
69
|
+
auth_request = AuthRequest(headers=headers)
|
|
70
|
+
auth_result = await self.auth_provider.authenticate(auth_request)
|
|
71
|
+
# Reject unauthenticated requests (unless NoAuth)
|
|
72
|
+
if not isinstance(self.auth_provider, NoAuth) and not auth_result.authenticated:
|
|
73
|
+
error_msg = json.dumps({
|
|
74
|
+
"error": auth_result.error or "Authentication failed",
|
|
75
|
+
})
|
|
76
|
+
await event_queue.enqueue_event(new_agent_text_message(error_msg))
|
|
77
|
+
return
|
|
77
78
|
|
|
78
79
|
# Extract message and parts
|
|
79
80
|
message, parts = self._extract_message_and_parts(context)
|
|
@@ -88,9 +89,10 @@ class LiteAgentExecutor(AgentExecutor):
|
|
|
88
89
|
message=message,
|
|
89
90
|
)
|
|
90
91
|
|
|
91
|
-
# Store parts in metadata for skill access
|
|
92
|
+
# Store parts and auth result in metadata for skill access
|
|
92
93
|
ctx.metadata["parts"] = parts
|
|
93
94
|
ctx.metadata["event_queue"] = event_queue
|
|
95
|
+
ctx.metadata["auth_result"] = auth_result
|
|
94
96
|
|
|
95
97
|
# Define final handler
|
|
96
98
|
async def final_handler(ctx: MiddlewareContext) -> Any:
|
|
@@ -173,6 +175,10 @@ class LiteAgentExecutor(AgentExecutor):
|
|
|
173
175
|
param_name = skill_def.interaction_param or "ctx"
|
|
174
176
|
params[param_name] = interaction_ctx
|
|
175
177
|
|
|
178
|
+
if skill_def.needs_auth:
|
|
179
|
+
param_name = skill_def.auth_param or "auth"
|
|
180
|
+
params[param_name] = metadata.get("auth_result")
|
|
181
|
+
|
|
176
182
|
# Call the handler
|
|
177
183
|
handler = skill_def.handler
|
|
178
184
|
|
|
@@ -212,7 +218,8 @@ class LiteAgentExecutor(AgentExecutor):
|
|
|
212
218
|
# Skip special context types
|
|
213
219
|
from .tasks import TaskContext as _TaskContext
|
|
214
220
|
from .human_loop import InteractionContext as _InteractionContext
|
|
215
|
-
|
|
221
|
+
from .auth import AuthResult as _AuthResult
|
|
222
|
+
if _is_or_subclass(param_type, _TaskContext) or _is_or_subclass(param_type, _InteractionContext) or _is_or_subclass(param_type, _AuthResult):
|
|
216
223
|
continue
|
|
217
224
|
|
|
218
225
|
# Convert FilePart
|
|
@@ -345,7 +352,7 @@ class LiteAgentExecutor(AgentExecutor):
|
|
|
345
352
|
if asyncio.iscoroutinefunction(handler):
|
|
346
353
|
return await handler(*args, **kwargs)
|
|
347
354
|
else:
|
|
348
|
-
loop = asyncio.
|
|
355
|
+
loop = asyncio.get_running_loop()
|
|
349
356
|
return await loop.run_in_executor(
|
|
350
357
|
None,
|
|
351
358
|
lambda: handler(*args, **kwargs)
|
|
@@ -214,7 +214,16 @@ class AgentTestClient:
|
|
|
214
214
|
result = await gen
|
|
215
215
|
results.append(result)
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
# Handle both sync and async calling contexts
|
|
218
|
+
try:
|
|
219
|
+
asyncio.get_running_loop()
|
|
220
|
+
# Already in an async context — run in a separate thread
|
|
221
|
+
import concurrent.futures
|
|
222
|
+
with concurrent.futures.ThreadPoolExecutor(1) as pool:
|
|
223
|
+
pool.submit(asyncio.run, run_handler()).result()
|
|
224
|
+
except RuntimeError:
|
|
225
|
+
# No running loop — safe to use asyncio.run()
|
|
226
|
+
asyncio.run(run_handler())
|
|
218
227
|
return results
|
|
219
228
|
|
|
220
229
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Helper functions for A2A Lite.
|
|
3
3
|
"""
|
|
4
|
+
import typing
|
|
4
5
|
from typing import Any, Dict, Type, get_origin, get_args, Union
|
|
5
6
|
import inspect
|
|
6
7
|
|
|
@@ -103,7 +104,10 @@ def extract_function_schemas(func) -> tuple[Dict[str, Any], Dict[str, Any]]:
|
|
|
103
104
|
Tuple of (input_schema, output_schema)
|
|
104
105
|
"""
|
|
105
106
|
sig = inspect.signature(func)
|
|
106
|
-
|
|
107
|
+
try:
|
|
108
|
+
hints = typing.get_type_hints(func)
|
|
109
|
+
except Exception:
|
|
110
|
+
hints = getattr(func, '__annotations__', {})
|
|
107
111
|
|
|
108
112
|
# Build input schema from parameters
|
|
109
113
|
properties = {}
|
|
@@ -9,7 +9,7 @@ Simplifies sending notifications when tasks complete:
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from dataclasses import dataclass
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
13
|
from typing import Any, Callable, Dict, List, Optional
|
|
14
14
|
import asyncio
|
|
15
15
|
import json
|
|
@@ -19,15 +19,11 @@ import json
|
|
|
19
19
|
class WebhookConfig:
|
|
20
20
|
"""Configuration for a webhook endpoint."""
|
|
21
21
|
url: str
|
|
22
|
-
headers: Dict[str, str] =
|
|
22
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
23
23
|
retry_count: int = 3
|
|
24
24
|
retry_delay: float = 1.0
|
|
25
25
|
timeout: float = 30.0
|
|
26
26
|
|
|
27
|
-
def __post_init__(self):
|
|
28
|
-
if self.headers is None:
|
|
29
|
-
self.headers = {}
|
|
30
|
-
|
|
31
27
|
|
|
32
28
|
class WebhookClient:
|
|
33
29
|
"""
|
|
@@ -9,6 +9,7 @@ from a2a_lite.auth import (
|
|
|
9
9
|
APIKeyAuth,
|
|
10
10
|
BearerAuth,
|
|
11
11
|
CompositeAuth,
|
|
12
|
+
require_auth,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -290,3 +291,88 @@ class TestAuthIntegration:
|
|
|
290
291
|
|
|
291
292
|
result_text = data.get("result", {}).get("parts", [{}])[0].get("text", "")
|
|
292
293
|
assert "secret: hello" not in result_text
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestRequireAuth:
|
|
297
|
+
"""Test that require_auth decorator receives AuthResult from executor."""
|
|
298
|
+
|
|
299
|
+
def test_require_auth_receives_auth_result(self):
|
|
300
|
+
"""Skills decorated with require_auth should receive the AuthResult."""
|
|
301
|
+
from a2a_lite import Agent
|
|
302
|
+
from a2a_lite.testing import AgentTestClient
|
|
303
|
+
from starlette.testclient import TestClient
|
|
304
|
+
import json
|
|
305
|
+
from uuid import uuid4
|
|
306
|
+
|
|
307
|
+
agent = Agent(
|
|
308
|
+
name="AuthTest",
|
|
309
|
+
description="require_auth test",
|
|
310
|
+
auth=APIKeyAuth(keys=["my-key"]),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@agent.skill("admin")
|
|
314
|
+
@require_auth(scopes=["admin"])
|
|
315
|
+
async def admin_action(data: str, auth: AuthResult) -> str:
|
|
316
|
+
return f"admin:{auth.user_id}:{data}"
|
|
317
|
+
|
|
318
|
+
app = agent.get_app()
|
|
319
|
+
client = TestClient(app)
|
|
320
|
+
|
|
321
|
+
# Without auth — should be rejected at the gate
|
|
322
|
+
request_body = {
|
|
323
|
+
"jsonrpc": "2.0",
|
|
324
|
+
"method": "message/send",
|
|
325
|
+
"id": uuid4().hex,
|
|
326
|
+
"params": {
|
|
327
|
+
"message": {
|
|
328
|
+
"role": "user",
|
|
329
|
+
"parts": [{"type": "text", "text": json.dumps({"skill": "admin", "params": {"data": "hello"}})}],
|
|
330
|
+
"messageId": uuid4().hex,
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
response = client.post("/", json=request_body)
|
|
335
|
+
result_text = response.json().get("result", {}).get("parts", [{}])[0].get("text", "")
|
|
336
|
+
assert "admin:" not in result_text
|
|
337
|
+
|
|
338
|
+
# With valid auth — require_auth checks scopes (no admin scope, so should fail)
|
|
339
|
+
response = client.post("/", json=request_body, headers={"X-API-Key": "my-key"})
|
|
340
|
+
result_text = response.json().get("result", {}).get("parts", [{}])[0].get("text", "")
|
|
341
|
+
assert "Insufficient permissions" in result_text or "error" in result_text.lower()
|
|
342
|
+
|
|
343
|
+
def test_auth_param_injected_without_decorator(self):
|
|
344
|
+
"""Skills with auth: AuthResult parameter should receive it directly."""
|
|
345
|
+
from a2a_lite import Agent
|
|
346
|
+
from starlette.testclient import TestClient
|
|
347
|
+
import json
|
|
348
|
+
from uuid import uuid4
|
|
349
|
+
|
|
350
|
+
agent = Agent(
|
|
351
|
+
name="AuthTest2",
|
|
352
|
+
description="auth param test",
|
|
353
|
+
auth=APIKeyAuth(keys=["my-key"]),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
@agent.skill("whoami")
|
|
357
|
+
async def whoami(auth: AuthResult) -> str:
|
|
358
|
+
return f"user:{auth.user_id}"
|
|
359
|
+
|
|
360
|
+
app = agent.get_app()
|
|
361
|
+
client = TestClient(app)
|
|
362
|
+
|
|
363
|
+
request_body = {
|
|
364
|
+
"jsonrpc": "2.0",
|
|
365
|
+
"method": "message/send",
|
|
366
|
+
"id": uuid4().hex,
|
|
367
|
+
"params": {
|
|
368
|
+
"message": {
|
|
369
|
+
"role": "user",
|
|
370
|
+
"parts": [{"type": "text", "text": json.dumps({"skill": "whoami", "params": {}})}],
|
|
371
|
+
"messageId": uuid4().hex,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
response = client.post("/", json=request_body, headers={"X-API-Key": "my-key"})
|
|
377
|
+
result_text = response.json().get("result", {}).get("parts", [{}])[0].get("text", "")
|
|
378
|
+
assert result_text.startswith("user:")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|