a2a-lite 0.2.2__py3-none-any.whl → 0.2.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.
- a2a_lite/__init__.py +7 -36
- a2a_lite/agent.py +2 -64
- a2a_lite/cli.py +2 -43
- a2a_lite/decorators.py +0 -3
- a2a_lite/executor.py +1 -9
- a2a_lite/streaming.py +0 -29
- {a2a_lite-0.2.2.dist-info → a2a_lite-0.2.4.dist-info}/METADATA +40 -95
- a2a_lite-0.2.4.dist-info/RECORD +16 -0
- a2a_lite/discovery.py +0 -152
- a2a_lite/human_loop.py +0 -284
- a2a_lite/webhooks.py +0 -228
- a2a_lite-0.2.2.dist-info/RECORD +0 -19
- {a2a_lite-0.2.2.dist-info → a2a_lite-0.2.4.dist-info}/WHEEL +0 -0
- {a2a_lite-0.2.2.dist-info → a2a_lite-0.2.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-lite
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Simplified wrapper for Google's A2A Protocol SDK
|
|
5
5
|
Author: A2A Lite Contributors
|
|
6
|
-
License-Expression:
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Keywords: a2a,agents,ai,protocol,sdk
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
|
-
Classifier: License :: OSI Approved ::
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -21,8 +21,6 @@ Requires-Dist: rich>=13.0
|
|
|
21
21
|
Requires-Dist: starlette>=0.40.0
|
|
22
22
|
Requires-Dist: typer>=0.9.0
|
|
23
23
|
Requires-Dist: uvicorn>=0.30.0
|
|
24
|
-
Requires-Dist: watchfiles>=0.20.0
|
|
25
|
-
Requires-Dist: zeroconf>=0.80.0
|
|
26
24
|
Provides-Extra: dev
|
|
27
25
|
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
28
26
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
@@ -33,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
33
31
|
|
|
34
32
|
# A2A Lite - Python
|
|
35
33
|
|
|
36
|
-
**Build A2A agents in 8 lines. Add
|
|
34
|
+
**Build A2A agents in 8 lines. Add features when you need them.**
|
|
37
35
|
|
|
38
36
|
Wraps the official [A2A Python SDK](https://github.com/a2aproject/a2a-python) with a simple, intuitive API.
|
|
39
37
|
|
|
@@ -149,7 +147,6 @@ class User(BaseModel):
|
|
|
149
147
|
|
|
150
148
|
@agent.skill("create_user")
|
|
151
149
|
async def create_user(user: User) -> dict:
|
|
152
|
-
# 'user' is already a User instance — auto-converted from dict!
|
|
153
150
|
return {"id": 1, "name": user.name}
|
|
154
151
|
```
|
|
155
152
|
|
|
@@ -188,28 +185,13 @@ Built-in middleware:
|
|
|
188
185
|
```python
|
|
189
186
|
from a2a_lite import logging_middleware, timing_middleware, retry_middleware, rate_limit_middleware
|
|
190
187
|
|
|
191
|
-
agent.
|
|
192
|
-
agent.
|
|
193
|
-
agent.
|
|
194
|
-
agent.
|
|
188
|
+
agent.add_middleware(logging_middleware)
|
|
189
|
+
agent.add_middleware(timing_middleware)
|
|
190
|
+
agent.add_middleware(rate_limit_middleware(max_per_minute=60))
|
|
191
|
+
agent.add_middleware(retry_middleware(max_retries=3))
|
|
195
192
|
```
|
|
196
193
|
|
|
197
|
-
### Level 5:
|
|
198
|
-
|
|
199
|
-
```python
|
|
200
|
-
from a2a_lite import InteractionContext
|
|
201
|
-
|
|
202
|
-
@agent.skill("wizard")
|
|
203
|
-
async def wizard(ctx: InteractionContext) -> dict:
|
|
204
|
-
name = await ctx.ask("What's your name?")
|
|
205
|
-
role = await ctx.ask("Role?", options=["Dev", "Manager"])
|
|
206
|
-
|
|
207
|
-
if await ctx.confirm(f"Create {name} as {role}?"):
|
|
208
|
-
return {"created": name}
|
|
209
|
-
return {"cancelled": True}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Level 6: File Handling
|
|
194
|
+
### Level 5: File Handling
|
|
213
195
|
|
|
214
196
|
```python
|
|
215
197
|
from a2a_lite import FilePart
|
|
@@ -220,7 +202,7 @@ async def summarize(doc: FilePart) -> str:
|
|
|
220
202
|
return f"Summary: {content[:100]}..."
|
|
221
203
|
```
|
|
222
204
|
|
|
223
|
-
### Level
|
|
205
|
+
### Level 6: Task Tracking
|
|
224
206
|
|
|
225
207
|
```python
|
|
226
208
|
from a2a_lite import TaskContext
|
|
@@ -237,7 +219,7 @@ async def process(data: str, task: TaskContext) -> str:
|
|
|
237
219
|
return "Done!"
|
|
238
220
|
```
|
|
239
221
|
|
|
240
|
-
### Level
|
|
222
|
+
### Level 7: Authentication
|
|
241
223
|
|
|
242
224
|
```python
|
|
243
225
|
from a2a_lite import Agent, APIKeyAuth
|
|
@@ -262,14 +244,24 @@ agent = Agent(
|
|
|
262
244
|
auth=BearerAuth(secret="your-jwt-secret"),
|
|
263
245
|
)
|
|
264
246
|
|
|
265
|
-
# OAuth2
|
|
247
|
+
# OAuth2 (requires: pip install a2a-lite[oauth])
|
|
266
248
|
agent = Agent(
|
|
267
249
|
name="Bot", description="A bot",
|
|
268
250
|
auth=OAuth2Auth(issuer="https://auth.example.com", audience="my-api"),
|
|
269
251
|
)
|
|
270
252
|
```
|
|
271
253
|
|
|
272
|
-
|
|
254
|
+
Skills can receive auth results by type-hinting a parameter as `AuthResult`:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from a2a_lite.auth import AuthResult
|
|
258
|
+
|
|
259
|
+
@agent.skill("whoami")
|
|
260
|
+
async def whoami(auth: AuthResult) -> dict:
|
|
261
|
+
return {"user": auth.identity, "scheme": auth.scheme}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Level 8: CORS and Production Mode
|
|
273
265
|
|
|
274
266
|
```python
|
|
275
267
|
agent = Agent(
|
|
@@ -280,11 +272,11 @@ agent = Agent(
|
|
|
280
272
|
)
|
|
281
273
|
```
|
|
282
274
|
|
|
283
|
-
### Level
|
|
275
|
+
### Level 9: Completion Hooks
|
|
284
276
|
|
|
285
277
|
```python
|
|
286
278
|
@agent.on_complete
|
|
287
|
-
async def notify(skill_name, result):
|
|
279
|
+
async def notify(skill_name, result, ctx):
|
|
288
280
|
print(f"Skill {skill_name} completed with: {result}")
|
|
289
281
|
```
|
|
290
282
|
|
|
@@ -313,25 +305,16 @@ async def info(name: str, age: int) -> dict:
|
|
|
313
305
|
def test_simple_result():
|
|
314
306
|
client = AgentTestClient(agent)
|
|
315
307
|
result = client.call("greet", name="World")
|
|
316
|
-
# Simple values support direct equality
|
|
317
308
|
assert result == "Hello, World!"
|
|
318
309
|
|
|
319
310
|
|
|
320
311
|
def test_dict_result():
|
|
321
312
|
client = AgentTestClient(agent)
|
|
322
313
|
result = client.call("info", name="Alice", age=30)
|
|
323
|
-
# Access dict results via .data
|
|
324
314
|
assert result.data["name"] == "Alice"
|
|
325
315
|
assert result.data["age"] == 30
|
|
326
316
|
|
|
327
317
|
|
|
328
|
-
def test_text_access():
|
|
329
|
-
client = AgentTestClient(agent)
|
|
330
|
-
result = client.call("greet", name="World")
|
|
331
|
-
# .text gives the raw string
|
|
332
|
-
assert result.text == '"Hello, World!"'
|
|
333
|
-
|
|
334
|
-
|
|
335
318
|
def test_list_skills():
|
|
336
319
|
client = AgentTestClient(agent)
|
|
337
320
|
skills = client.list_skills()
|
|
@@ -379,59 +362,19 @@ def test_streaming():
|
|
|
379
362
|
|
|
380
363
|
---
|
|
381
364
|
|
|
382
|
-
## Task Store
|
|
383
|
-
|
|
384
|
-
The `TaskStore` provides async-safe task lifecycle management:
|
|
385
|
-
|
|
386
|
-
```python
|
|
387
|
-
from a2a_lite.tasks import TaskStore, TaskStatus
|
|
388
|
-
|
|
389
|
-
store = TaskStore()
|
|
390
|
-
|
|
391
|
-
# All operations are async and thread-safe
|
|
392
|
-
task = await store.create(task_id="task-1", skill="process")
|
|
393
|
-
task = await store.get("task-1")
|
|
394
|
-
await store.update("task-1", status=TaskStatus.WORKING, progress=0.5)
|
|
395
|
-
tasks = await store.list()
|
|
396
|
-
await store.delete("task-1")
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
## Agent Discovery
|
|
402
|
-
|
|
403
|
-
Find agents on your local network via mDNS:
|
|
404
|
-
|
|
405
|
-
```python
|
|
406
|
-
from a2a_lite import AgentDiscovery
|
|
407
|
-
|
|
408
|
-
# Advertise your agent
|
|
409
|
-
agent.run(port=8787, enable_discovery=True)
|
|
410
|
-
|
|
411
|
-
# Discover other agents
|
|
412
|
-
discovery = AgentDiscovery()
|
|
413
|
-
agents = await discovery.discover(timeout=5.0)
|
|
414
|
-
for a in agents:
|
|
415
|
-
print(f"{a.name} at {a.url}")
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
365
|
## CLI
|
|
421
366
|
|
|
422
367
|
```bash
|
|
423
368
|
a2a-lite init my-agent # Create new project
|
|
424
369
|
a2a-lite serve agent.py # Run agent from file
|
|
425
|
-
a2a-lite serve agent.py -r # Run with hot reload
|
|
426
370
|
a2a-lite inspect http://... # View agent capabilities
|
|
427
371
|
a2a-lite test http://... skill # Test a skill
|
|
428
|
-
a2a-lite discover # Find local agents
|
|
429
372
|
a2a-lite version # Show version
|
|
430
373
|
```
|
|
431
374
|
|
|
432
375
|
---
|
|
433
376
|
|
|
434
|
-
##
|
|
377
|
+
## API Reference
|
|
435
378
|
|
|
436
379
|
### Agent
|
|
437
380
|
|
|
@@ -445,7 +388,6 @@ Agent(
|
|
|
445
388
|
task_store: str | TaskStore = None, # "memory" or custom TaskStore
|
|
446
389
|
cors_origins: List[str] = None, # CORS allowed origins
|
|
447
390
|
production: bool = False, # Enable production warnings
|
|
448
|
-
enable_discovery: bool = False, # mDNS discovery
|
|
449
391
|
)
|
|
450
392
|
```
|
|
451
393
|
|
|
@@ -455,8 +397,11 @@ Agent(
|
|
|
455
397
|
|--------|-------------|
|
|
456
398
|
| `@agent.skill(name, **config)` | Register a skill via decorator |
|
|
457
399
|
| `@agent.middleware` | Register middleware via decorator |
|
|
458
|
-
| `agent.
|
|
400
|
+
| `agent.add_middleware(fn)` | Register middleware function |
|
|
459
401
|
| `@agent.on_complete` | Register completion hook |
|
|
402
|
+
| `@agent.on_startup` | Register startup hook |
|
|
403
|
+
| `@agent.on_shutdown` | Register shutdown hook |
|
|
404
|
+
| `@agent.on_error` | Register error handler |
|
|
460
405
|
| `agent.run(port=8787)` | Start the server |
|
|
461
406
|
| `agent.get_app()` | Get the ASGI app (for custom deployment) |
|
|
462
407
|
|
|
@@ -464,7 +409,7 @@ Agent(
|
|
|
464
409
|
|
|
465
410
|
```python
|
|
466
411
|
@agent.skill(
|
|
467
|
-
name: str,
|
|
412
|
+
name: str = None, # Skill name (defaults to function name)
|
|
468
413
|
description: str = None, # Human-readable description
|
|
469
414
|
tags: List[str] = None, # Categorization tags
|
|
470
415
|
streaming: bool = False, # Enable streaming
|
|
@@ -477,18 +422,19 @@ Agent(
|
|
|
477
422
|
|----------|-------|
|
|
478
423
|
| `APIKeyAuth(keys=[...])` | API key auth (keys hashed with SHA-256) |
|
|
479
424
|
| `BearerAuth(secret=...)` | JWT/Bearer token auth |
|
|
480
|
-
| `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth |
|
|
425
|
+
| `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth (requires `a2a-lite[oauth]`) |
|
|
481
426
|
| `NoAuth()` | No auth (default) |
|
|
482
427
|
|
|
483
428
|
### Special Parameter Types
|
|
484
429
|
|
|
485
|
-
These are auto-injected when detected in skill signatures:
|
|
430
|
+
These are auto-injected when detected in skill function signatures:
|
|
486
431
|
|
|
487
432
|
| Type | Description |
|
|
488
433
|
|------|-------------|
|
|
489
434
|
| `TaskContext` | Task lifecycle management (requires `task_store`) |
|
|
490
|
-
| `
|
|
435
|
+
| `AuthResult` | Authentication result injection |
|
|
491
436
|
| `FilePart` | File upload handling |
|
|
437
|
+
| `DataPart` | Structured data handling |
|
|
492
438
|
|
|
493
439
|
---
|
|
494
440
|
|
|
@@ -501,10 +447,9 @@ These are auto-injected when detected in skill signatures:
|
|
|
501
447
|
| [06_pydantic_models.py](examples/06_pydantic_models.py) | Auto Pydantic conversion |
|
|
502
448
|
| [08_streaming.py](examples/08_streaming.py) | Streaming responses |
|
|
503
449
|
| [09_testing.py](examples/09_testing.py) | Testing your agents |
|
|
504
|
-
| [
|
|
505
|
-
| [
|
|
506
|
-
| [
|
|
507
|
-
| [14_with_auth.py](examples/14_with_auth.py) | Authentication |
|
|
450
|
+
| [10_file_handling.py](examples/10_file_handling.py) | Handle files |
|
|
451
|
+
| [11_task_tracking.py](examples/11_task_tracking.py) | Progress updates |
|
|
452
|
+
| [12_with_auth.py](examples/12_with_auth.py) | Authentication |
|
|
508
453
|
|
|
509
454
|
---
|
|
510
455
|
|
|
@@ -516,13 +461,13 @@ A2A Lite wraps the official A2A Python SDK. Every feature maps to real A2A proto
|
|
|
516
461
|
|----------|--------------|
|
|
517
462
|
| `@agent.skill()` | Agent Skills |
|
|
518
463
|
| `streaming=True` | SSE Streaming |
|
|
519
|
-
| `InteractionContext.ask()` | `input-required` state |
|
|
520
464
|
| `TaskContext.update()` | Task lifecycle states |
|
|
521
465
|
| `FilePart` | A2A File parts |
|
|
466
|
+
| `DataPart` | A2A Data parts |
|
|
522
467
|
| `APIKeyAuth` / `BearerAuth` | Security schemes |
|
|
523
468
|
|
|
524
469
|
---
|
|
525
470
|
|
|
526
471
|
## License
|
|
527
472
|
|
|
528
|
-
|
|
473
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
a2a_lite/__init__.py,sha256=92WTqzqJV2FTBV0peiuLUdB9y5BZe37jvCT7E2k_vNQ,2642
|
|
2
|
+
a2a_lite/agent.py,sha256=2x4t4ysh10Qwjwp4S8pQehf8O3sINGoJIvq9YN9G-q0,15321
|
|
3
|
+
a2a_lite/auth.py,sha256=A1AMncM0cuWEAcysjumAhjd0lI_jMLmJpeYWuAPj30A,10181
|
|
4
|
+
a2a_lite/cli.py,sha256=5oAwZLR59ebxhuJjIYSHd_AskZqToQLRz45jAAlEBp0,7680
|
|
5
|
+
a2a_lite/decorators.py,sha256=rRzpeVW6mZTAFpNzZYDdraShbDdq7S32c4Z16edD4xw,979
|
|
6
|
+
a2a_lite/executor.py,sha256=pZx_3_JV34qWgbNDkzCWmT1qJRuBAxhrmz4luU7bDBc,13163
|
|
7
|
+
a2a_lite/middleware.py,sha256=c6jb9aFfyTf-JY6KjqaSgFJmpzqbHLC6Q1h9NNteqzo,5545
|
|
8
|
+
a2a_lite/parts.py,sha256=qVRiD-H9_NlMPk-R0gTUiGVQ77E2poiuBWAUyAyAoTI,6177
|
|
9
|
+
a2a_lite/streaming.py,sha256=6qLMUlbk61HBhtotxhSz5Yh4wL9OHeGt24o_J--RJvI,1626
|
|
10
|
+
a2a_lite/tasks.py,sha256=UpmDP-VGIQ1LodBNq4zx2pJElQ31gOJOAduHFBVyxOA,7039
|
|
11
|
+
a2a_lite/testing.py,sha256=M9IbLA6oUz1DokJ9Sc_r0gK43NNkU78IVkiBRuDFFCU,9393
|
|
12
|
+
a2a_lite/utils.py,sha256=AFLYQ4J-F7H_HeYWAeg8H3p9EOdDv4dOpju_ebrU5PI,3934
|
|
13
|
+
a2a_lite-0.2.4.dist-info/METADATA,sha256=qR6P0NOhPdbfavyIb66t7PvvjqcCapTsVdZMsLOsEmA,11366
|
|
14
|
+
a2a_lite-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
a2a_lite-0.2.4.dist-info/entry_points.txt,sha256=BONfFqZbCntNal2iwlTJAE09gCUvurfvqslMYVYh4is,46
|
|
16
|
+
a2a_lite-0.2.4.dist-info/RECORD,,
|
a2a_lite/discovery.py
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
mDNS-based local agent discovery using Zeroconf.
|
|
3
|
-
"""
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import asyncio
|
|
7
|
-
import logging
|
|
8
|
-
import socket
|
|
9
|
-
from typing import Dict, List, Optional
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf, ServiceInfo
|
|
15
|
-
from zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
SERVICE_TYPE = "_a2a-agent._tcp.local."
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass
|
|
22
|
-
class DiscoveredAgent:
|
|
23
|
-
"""Information about a discovered A2A agent."""
|
|
24
|
-
name: str
|
|
25
|
-
host: str
|
|
26
|
-
port: int
|
|
27
|
-
url: str
|
|
28
|
-
properties: Dict[str, str]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class AgentDiscovery:
|
|
32
|
-
"""
|
|
33
|
-
Discover A2A agents on the local network using mDNS.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(self):
|
|
37
|
-
self._discovered: Dict[str, DiscoveredAgent] = {}
|
|
38
|
-
self._zeroconf: Optional[Zeroconf] = None
|
|
39
|
-
self._service_info: Optional[ServiceInfo] = None
|
|
40
|
-
|
|
41
|
-
async def discover(self, timeout: float = 5.0) -> List[DiscoveredAgent]:
|
|
42
|
-
"""
|
|
43
|
-
Discover agents on the local network.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
timeout: How long to wait for discoveries
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
List of discovered agents
|
|
50
|
-
"""
|
|
51
|
-
self._discovered.clear()
|
|
52
|
-
|
|
53
|
-
async with AsyncZeroconf() as azc:
|
|
54
|
-
listener = _DiscoveryListener(self._discovered)
|
|
55
|
-
browser = AsyncServiceBrowser(
|
|
56
|
-
azc.zeroconf, SERVICE_TYPE, listener
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
await asyncio.sleep(timeout)
|
|
60
|
-
await browser.async_cancel()
|
|
61
|
-
|
|
62
|
-
return list(self._discovered.values())
|
|
63
|
-
|
|
64
|
-
def register(
|
|
65
|
-
self,
|
|
66
|
-
name: str,
|
|
67
|
-
port: int,
|
|
68
|
-
properties: Optional[Dict[str, str]] = None,
|
|
69
|
-
) -> ServiceInfo:
|
|
70
|
-
"""
|
|
71
|
-
Register this agent for discovery.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
name: Agent name
|
|
75
|
-
port: Port the agent is running on
|
|
76
|
-
properties: Additional properties to advertise
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
ServiceInfo for unregistration
|
|
80
|
-
"""
|
|
81
|
-
hostname = socket.gethostname()
|
|
82
|
-
local_ip = self._get_local_ip()
|
|
83
|
-
|
|
84
|
-
# Sanitize name for mDNS (remove spaces and special chars)
|
|
85
|
-
safe_name = "".join(c if c.isalnum() or c == "-" else "-" for c in name)
|
|
86
|
-
|
|
87
|
-
info = ServiceInfo(
|
|
88
|
-
SERVICE_TYPE,
|
|
89
|
-
f"{safe_name}.{SERVICE_TYPE}",
|
|
90
|
-
addresses=[socket.inet_aton(local_ip)],
|
|
91
|
-
port=port,
|
|
92
|
-
properties=properties or {},
|
|
93
|
-
server=f"{hostname}.local.",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
self._zeroconf = Zeroconf()
|
|
97
|
-
self._zeroconf.register_service(info)
|
|
98
|
-
self._service_info = info
|
|
99
|
-
|
|
100
|
-
return info
|
|
101
|
-
|
|
102
|
-
def unregister(self) -> None:
|
|
103
|
-
"""Unregister this agent from discovery."""
|
|
104
|
-
if self._zeroconf and self._service_info:
|
|
105
|
-
self._zeroconf.unregister_service(self._service_info)
|
|
106
|
-
self._zeroconf.close()
|
|
107
|
-
self._zeroconf = None
|
|
108
|
-
self._service_info = None
|
|
109
|
-
|
|
110
|
-
def _get_local_ip(self) -> str:
|
|
111
|
-
"""Get local IP address."""
|
|
112
|
-
try:
|
|
113
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
114
|
-
s.connect(("8.8.8.8", 80))
|
|
115
|
-
ip = s.getsockname()[0]
|
|
116
|
-
s.close()
|
|
117
|
-
return ip
|
|
118
|
-
except Exception:
|
|
119
|
-
logger.debug("Could not detect local IP, falling back to 127.0.0.1")
|
|
120
|
-
return "127.0.0.1"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class _DiscoveryListener(ServiceListener):
|
|
124
|
-
"""Internal listener for mDNS service discovery."""
|
|
125
|
-
|
|
126
|
-
def __init__(self, discovered: Dict[str, DiscoveredAgent]):
|
|
127
|
-
self.discovered = discovered
|
|
128
|
-
|
|
129
|
-
def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
|
130
|
-
info = zc.get_service_info(type_, name)
|
|
131
|
-
if info:
|
|
132
|
-
agent_name = name.replace(f".{SERVICE_TYPE}", "")
|
|
133
|
-
host = socket.inet_ntoa(info.addresses[0]) if info.addresses else "localhost"
|
|
134
|
-
port = info.port
|
|
135
|
-
|
|
136
|
-
self.discovered[name] = DiscoveredAgent(
|
|
137
|
-
name=agent_name,
|
|
138
|
-
host=host,
|
|
139
|
-
port=port,
|
|
140
|
-
url=f"http://{host}:{port}",
|
|
141
|
-
properties={
|
|
142
|
-
k.decode() if isinstance(k, bytes) else k:
|
|
143
|
-
v.decode() if isinstance(v, bytes) else str(v)
|
|
144
|
-
for k, v in info.properties.items()
|
|
145
|
-
},
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
|
149
|
-
self.discovered.pop(name, None)
|
|
150
|
-
|
|
151
|
-
def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
|
152
|
-
self.add_service(zc, type_, name)
|