Skip to content

Commit 076d83d

Browse files
committed
feat(hawk-sdk-python): adopt agentscope patterns — toolkit, plan, tracing, eval, discovery, memory
Add 6 new modules inspired by agentscope-ai/agentscope: - toolkit: tool groups, middleware chain, async background execution - plan: plan-as-tools with contextual hints for autonomous steering - tracing: OTel decorator-based tracing (zero-cost when disabled) - evaluate: agent benchmarking framework with N-run aggregation - discovery: A2A agent discovery (file, HTTP well-known, composite) - memory_tools: voluntary record/retrieve/forget as agent tools
1 parent 9e4780d commit 076d83d

7 files changed

Lines changed: 1591 additions & 0 deletions

File tree

src/hawk/__init__.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,19 @@
1313
RateLimitError,
1414
ServiceUnavailableError,
1515
)
16+
from .plan import Plan, PlanNotebook, SubTask, SubTaskState
1617
from .retry import DEFAULT_RETRY_CONFIG, RetryConfig
1718
from .streaming import AsyncStreamReader, StreamReader
1819
from .tools import Tool, chat_with_tools, chat_with_tools_async, tool
20+
from .toolkit import BackgroundTask, ToolGroup, Toolkit
21+
from .tracing import (
22+
configure_tracing,
23+
detect_provider,
24+
is_tracing_enabled,
25+
trace,
26+
trace_chat,
27+
trace_tool,
28+
)
1929
from .types import (
2030
ChatRequest,
2131
ChatResponse,
@@ -31,6 +41,21 @@
3141
ToolCall,
3242
Usage,
3343
)
44+
from .discovery import (
45+
AgentCard,
46+
AgentResolver,
47+
CompositeResolver,
48+
FileResolver,
49+
WellKnownResolver,
50+
)
51+
from .evaluate import (
52+
BenchmarkResults,
53+
EvalResult,
54+
EvalTask,
55+
run_benchmark,
56+
run_benchmark_async,
57+
)
58+
from .memory_tools import MemoryTools
3459
from .workflow import AsyncWorkflow, Workflow
3560

3661
__all__ = [
@@ -51,6 +76,15 @@
5176
"tool",
5277
"chat_with_tools",
5378
"chat_with_tools_async",
79+
# Plan
80+
"PlanNotebook",
81+
"Plan",
82+
"SubTask",
83+
"SubTaskState",
84+
# Toolkit
85+
"Toolkit",
86+
"ToolGroup",
87+
"BackgroundTask",
5488
# Workflow
5589
"Workflow",
5690
"AsyncWorkflow",
@@ -71,6 +105,13 @@
71105
"StreamEventType",
72106
"ToolCall",
73107
"Usage",
108+
# Tracing
109+
"configure_tracing",
110+
"detect_provider",
111+
"is_tracing_enabled",
112+
"trace",
113+
"trace_chat",
114+
"trace_tool",
74115
# Errors
75116
"HawkAPIError",
76117
"BadRequestError",
@@ -80,4 +121,18 @@
80121
"RateLimitError",
81122
"InternalServerError",
82123
"ServiceUnavailableError",
124+
# Evaluate
125+
"EvalTask",
126+
"EvalResult",
127+
"BenchmarkResults",
128+
"run_benchmark",
129+
"run_benchmark_async",
130+
# Discovery
131+
"AgentCard",
132+
"AgentResolver",
133+
"FileResolver",
134+
"WellKnownResolver",
135+
"CompositeResolver",
136+
# Memory
137+
"MemoryTools",
83138
]

src/hawk/discovery.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Agent-to-Agent discovery protocol for Hawk.
2+
3+
Enables agents to discover and communicate with other agents via
4+
multiple resolution strategies (HTTP well-known, file-based, registry).
5+
6+
Usage:
7+
from hawk.discovery import AgentCard, WellKnownResolver, FileResolver
8+
9+
resolver = WellKnownResolver()
10+
card = await resolver.resolve("assistant-agent")
11+
# card.endpoint -> "http://localhost:8080/v1/chat"
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import json
17+
import os
18+
from dataclasses import dataclass, field
19+
from typing import Any, Optional, Protocol
20+
21+
22+
@dataclass
23+
class AgentCard:
24+
"""Describes a discoverable agent's capabilities and endpoint."""
25+
name: str
26+
endpoint: str
27+
description: str = ""
28+
capabilities: list[str] = field(default_factory=list)
29+
version: str = "1.0"
30+
metadata: dict[str, Any] = field(default_factory=dict)
31+
32+
def to_dict(self) -> dict[str, Any]:
33+
return {
34+
"name": self.name,
35+
"endpoint": self.endpoint,
36+
"description": self.description,
37+
"capabilities": self.capabilities,
38+
"version": self.version,
39+
"metadata": self.metadata,
40+
}
41+
42+
@classmethod
43+
def from_dict(cls, data: dict[str, Any]) -> "AgentCard":
44+
return cls(
45+
name=data["name"],
46+
endpoint=data["endpoint"],
47+
description=data.get("description", ""),
48+
capabilities=data.get("capabilities", []),
49+
version=data.get("version", "1.0"),
50+
metadata=data.get("metadata", {}),
51+
)
52+
53+
54+
class AgentResolver(Protocol):
55+
"""Protocol for agent discovery resolvers."""
56+
57+
async def resolve(self, agent_name: str) -> Optional[AgentCard]:
58+
"""Resolve an agent name to its card."""
59+
...
60+
61+
async def list_agents(self) -> list[AgentCard]:
62+
"""List all known agents."""
63+
...
64+
65+
async def register(self, card: AgentCard) -> None:
66+
"""Register an agent card."""
67+
...
68+
69+
70+
class FileResolver:
71+
"""File-based agent discovery for local development.
72+
73+
Reads agent cards from a JSON file.
74+
75+
Usage:
76+
resolver = FileResolver("/path/to/agents.json")
77+
card = await resolver.resolve("my-agent")
78+
"""
79+
80+
def __init__(self, path: str = ".hawk/agents.json") -> None:
81+
self._path = path
82+
self._cards: dict[str, AgentCard] = {}
83+
self._load()
84+
85+
def _load(self) -> None:
86+
if os.path.exists(self._path):
87+
with open(self._path) as f:
88+
data = json.load(f)
89+
for entry in data.get("agents", []):
90+
card = AgentCard.from_dict(entry)
91+
self._cards[card.name] = card
92+
93+
def _save(self) -> None:
94+
os.makedirs(os.path.dirname(self._path) or ".", exist_ok=True)
95+
data = {"agents": [c.to_dict() for c in self._cards.values()]}
96+
with open(self._path, "w") as f:
97+
json.dump(data, f, indent=2)
98+
99+
async def resolve(self, agent_name: str) -> Optional[AgentCard]:
100+
return self._cards.get(agent_name)
101+
102+
async def list_agents(self) -> list[AgentCard]:
103+
return list(self._cards.values())
104+
105+
async def register(self, card: AgentCard) -> None:
106+
self._cards[card.name] = card
107+
self._save()
108+
109+
110+
class WellKnownResolver:
111+
"""HTTP-based agent discovery via well-known URLs.
112+
113+
Discovers agents by fetching {base_url}/.well-known/agent.json
114+
115+
Usage:
116+
resolver = WellKnownResolver(["http://localhost:8080", "http://agent2:8080"])
117+
card = await resolver.resolve("assistant")
118+
"""
119+
120+
def __init__(self, base_urls: Optional[list[str]] = None) -> None:
121+
self._base_urls = base_urls or []
122+
self._cache: dict[str, AgentCard] = {}
123+
124+
async def resolve(self, agent_name: str) -> Optional[AgentCard]:
125+
if agent_name in self._cache:
126+
return self._cache[agent_name]
127+
128+
for url in self._base_urls:
129+
card = await self._fetch_card(url)
130+
if card and card.name == agent_name:
131+
self._cache[agent_name] = card
132+
return card
133+
return None
134+
135+
async def list_agents(self) -> list[AgentCard]:
136+
cards = []
137+
for url in self._base_urls:
138+
card = await self._fetch_card(url)
139+
if card:
140+
cards.append(card)
141+
return cards
142+
143+
async def register(self, card: AgentCard) -> None:
144+
self._cache[card.name] = card
145+
if card.endpoint not in self._base_urls:
146+
self._base_urls.append(card.endpoint)
147+
148+
async def _fetch_card(self, base_url: str) -> Optional[AgentCard]:
149+
try:
150+
import httpx
151+
url = f"{base_url.rstrip('/')}/.well-known/agent.json"
152+
async with httpx.AsyncClient(timeout=5.0) as client:
153+
resp = await client.get(url)
154+
if resp.status_code == 200:
155+
return AgentCard.from_dict(resp.json())
156+
except Exception:
157+
pass
158+
return None
159+
160+
161+
class CompositeResolver:
162+
"""Chains multiple resolvers, returning the first match.
163+
164+
Usage:
165+
resolver = CompositeResolver([
166+
FileResolver(".hawk/agents.json"),
167+
WellKnownResolver(["http://localhost:8080"]),
168+
])
169+
card = await resolver.resolve("my-agent")
170+
"""
171+
172+
def __init__(self, resolvers: list[Any]) -> None:
173+
self._resolvers = resolvers
174+
175+
async def resolve(self, agent_name: str) -> Optional[AgentCard]:
176+
for resolver in self._resolvers:
177+
card = await resolver.resolve(agent_name)
178+
if card:
179+
return card
180+
return None
181+
182+
async def list_agents(self) -> list[AgentCard]:
183+
seen = set()
184+
cards = []
185+
for resolver in self._resolvers:
186+
for card in await resolver.list_agents():
187+
if card.name not in seen:
188+
seen.add(card.name)
189+
cards.append(card)
190+
return cards
191+
192+
async def register(self, card: AgentCard) -> None:
193+
if self._resolvers:
194+
await self._resolvers[0].register(card)

0 commit comments

Comments
 (0)