agentdna-sdk 0.2.0__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.
- agentdna/__init__.py +85 -0
- agentdna/cli.py +274 -0
- agentdna/client.py +125 -0
- agentdna/discovery.py +162 -0
- agentdna/marketplace.py +100 -0
- agentdna/models.py +122 -0
- agentdna/plugins/__init__.py +20 -0
- agentdna/plugins/crewai.py +185 -0
- agentdna/plugins/langchain.py +183 -0
- agentdna/plugins/observe.py +413 -0
- agentdna/py.typed +1 -0
- agentdna/registry.py +122 -0
- agentdna/sandbox/__init__.py +1 -0
- agentdna/sandbox/verifier.py +558 -0
- agentdna/trust/__init__.py +1 -0
- agentdna/trust/evaluator.py +208 -0
- agentdna/trust/scorer.py +450 -0
- agentdna_sdk-0.2.0.dist-info/METADATA +197 -0
- agentdna_sdk-0.2.0.dist-info/RECORD +21 -0
- agentdna_sdk-0.2.0.dist-info/WHEEL +4 -0
- agentdna_sdk-0.2.0.dist-info/entry_points.txt +2 -0
agentdna/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
🧬 AgentDNA — Sentry for AI Agents
|
|
3
|
+
|
|
4
|
+
One-line observability for any Python agent.
|
|
5
|
+
No framework required. No API key. No network calls.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from agentdna import observe, get_stats
|
|
9
|
+
|
|
10
|
+
@observe
|
|
11
|
+
def my_agent(prompt):
|
|
12
|
+
return llm.call(prompt)
|
|
13
|
+
|
|
14
|
+
# Check stats anytime (persists across restarts)
|
|
15
|
+
print(get_stats())
|
|
16
|
+
|
|
17
|
+
# Or use the CLI:
|
|
18
|
+
# agentdna stats
|
|
19
|
+
# agentdna stats my_agent
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.2.0"
|
|
23
|
+
|
|
24
|
+
# ⭐ Primary exports — zero dependencies (uses only stdlib sqlite3)
|
|
25
|
+
from agentdna.plugins.observe import observe, get_stats, reset_stats, export_stats
|
|
26
|
+
|
|
27
|
+
# Everything else is lazy-imported to avoid requiring httpx/PyYAML
|
|
28
|
+
# for users who only want observability.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name: str):
|
|
32
|
+
"""Lazy imports — only load when actually used."""
|
|
33
|
+
_lazy = {
|
|
34
|
+
# Models
|
|
35
|
+
"Agent": ("agentdna.models", "Agent"),
|
|
36
|
+
"AgentSearchResult": ("agentdna.models", "AgentSearchResult"),
|
|
37
|
+
"Capability": ("agentdna.models", "Capability"),
|
|
38
|
+
"Pricing": ("agentdna.models", "Pricing"),
|
|
39
|
+
"TrustScore": ("agentdna.models", "TrustScore"),
|
|
40
|
+
"TaskResult": ("agentdna.models", "TaskResult"),
|
|
41
|
+
# Registry
|
|
42
|
+
"register_agent": ("agentdna.registry", "register_agent"),
|
|
43
|
+
"load_agent_card": ("agentdna.registry", "load_agent_card"),
|
|
44
|
+
"generate_agent_card": ("agentdna.registry", "generate_agent_card"),
|
|
45
|
+
# Discovery
|
|
46
|
+
"find_agent": ("agentdna.discovery", "find_agent"),
|
|
47
|
+
"search_agents": ("agentdna.discovery", "search_agents"),
|
|
48
|
+
# Marketplace
|
|
49
|
+
"hire_agent": ("agentdna.marketplace", "hire_agent"),
|
|
50
|
+
"hire_agent_sync": ("agentdna.marketplace", "hire_agent_sync"),
|
|
51
|
+
# Client
|
|
52
|
+
"AgentDNAClient": ("agentdna.client", "AgentDNAClient"),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if name in _lazy:
|
|
56
|
+
module_path, attr_name = _lazy[name]
|
|
57
|
+
from importlib import import_module
|
|
58
|
+
mod = import_module(module_path)
|
|
59
|
+
return getattr(mod, attr_name)
|
|
60
|
+
|
|
61
|
+
raise AttributeError(f"module 'agentdna' has no attribute {name!r}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
# ⭐ Core — observability (zero dependencies)
|
|
66
|
+
"observe",
|
|
67
|
+
"get_stats",
|
|
68
|
+
"reset_stats",
|
|
69
|
+
"export_stats",
|
|
70
|
+
# Everything else (lazy, requires httpx/PyYAML)
|
|
71
|
+
"AgentDNAClient",
|
|
72
|
+
"Agent",
|
|
73
|
+
"AgentSearchResult",
|
|
74
|
+
"Capability",
|
|
75
|
+
"Pricing",
|
|
76
|
+
"TrustScore",
|
|
77
|
+
"TaskResult",
|
|
78
|
+
"register_agent",
|
|
79
|
+
"load_agent_card",
|
|
80
|
+
"generate_agent_card",
|
|
81
|
+
"find_agent",
|
|
82
|
+
"search_agents",
|
|
83
|
+
"hire_agent",
|
|
84
|
+
"hire_agent_sync",
|
|
85
|
+
]
|
agentdna/cli.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
🧬 AgentDNA CLI — Sentry for AI Agents
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
agentdna stats [func] View observability stats
|
|
7
|
+
agentdna stats --reset [func] Reset stats
|
|
8
|
+
agentdna stats --export json Export stats
|
|
9
|
+
agentdna search <query> Search for agents
|
|
10
|
+
agentdna register [path] Register your agent
|
|
11
|
+
agentdna init [name] Generate agentdna.yaml
|
|
12
|
+
agentdna trust <agent-id> View trust score
|
|
13
|
+
agentdna status <agent-id> Check agent health
|
|
14
|
+
agentdna review <agent-id> Submit a review
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
_CLI_VERSION = "0.2.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
@click.version_option(version=_CLI_VERSION)
|
|
26
|
+
def cli():
|
|
27
|
+
"""🧬 AgentDNA — Sentry for AI Agents"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --- Stats Command (the money maker) ---
|
|
32
|
+
|
|
33
|
+
@cli.command()
|
|
34
|
+
@click.argument("func_name", required=False, default=None)
|
|
35
|
+
@click.option("--reset", is_flag=True, help="Reset stats for this function (or all)")
|
|
36
|
+
@click.option("--export", "export_format", type=click.Choice(["json", "csv"]), help="Export format")
|
|
37
|
+
@click.option("--db", "db_path", help="Path to SQLite database")
|
|
38
|
+
def stats(func_name, reset, export_format, db_path):
|
|
39
|
+
"""📊 View observability stats for your agents."""
|
|
40
|
+
import os
|
|
41
|
+
if db_path:
|
|
42
|
+
os.environ["AGENTDNA_DB_PATH"] = db_path
|
|
43
|
+
|
|
44
|
+
from agentdna.plugins.observe import get_stats, reset_stats, export_stats
|
|
45
|
+
|
|
46
|
+
if reset:
|
|
47
|
+
if func_name:
|
|
48
|
+
reset_stats(func_name)
|
|
49
|
+
click.echo(f"✅ Reset stats for: {func_name}")
|
|
50
|
+
else:
|
|
51
|
+
reset_stats()
|
|
52
|
+
click.echo("✅ Reset all stats")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if export_format:
|
|
56
|
+
output = export_stats(func_name, format=export_format)
|
|
57
|
+
click.echo(output)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
data = get_stats(func_name)
|
|
61
|
+
|
|
62
|
+
if not data:
|
|
63
|
+
click.echo("📊 No data yet. Add @observe to your agent functions first.")
|
|
64
|
+
click.echo()
|
|
65
|
+
click.echo(" from agentdna import observe")
|
|
66
|
+
click.echo()
|
|
67
|
+
click.echo(" @observe")
|
|
68
|
+
click.echo(" def my_agent(prompt):")
|
|
69
|
+
click.echo(" return result")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Single function stats
|
|
73
|
+
if func_name and isinstance(data, dict) and "total_calls" in data:
|
|
74
|
+
_print_stats(func_name, data)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# All functions
|
|
78
|
+
click.echo(f"\n📊 AgentDNA — {len(data)} observed function(s)\n")
|
|
79
|
+
for name, s in data.items():
|
|
80
|
+
_print_stats(name, s, compact=True)
|
|
81
|
+
click.echo()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _print_stats(name: str, s: dict, compact: bool = False):
|
|
85
|
+
"""Pretty-print stats for a function."""
|
|
86
|
+
total = s.get("total_calls", 0)
|
|
87
|
+
success_rate = s.get("success_rate", 0)
|
|
88
|
+
avg_latency = s.get("avg_latency_ms", 0)
|
|
89
|
+
p50 = s.get("p50_latency_ms", 0)
|
|
90
|
+
p95 = s.get("p95_latency_ms", 0)
|
|
91
|
+
p99 = s.get("p99_latency_ms", 0)
|
|
92
|
+
failed = s.get("failed_calls", 0)
|
|
93
|
+
errors = s.get("error_types", {})
|
|
94
|
+
first = s.get("first_seen", "?")
|
|
95
|
+
last = s.get("last_seen", "?")
|
|
96
|
+
|
|
97
|
+
# Health indicator
|
|
98
|
+
if success_rate >= 0.95:
|
|
99
|
+
health = "✅ Healthy"
|
|
100
|
+
elif success_rate >= 0.80:
|
|
101
|
+
health = "⚠️ Degraded"
|
|
102
|
+
else:
|
|
103
|
+
health = "🔴 Unhealthy"
|
|
104
|
+
|
|
105
|
+
if compact:
|
|
106
|
+
bar = "█" * int(success_rate * 10) + "░" * (10 - int(success_rate * 10))
|
|
107
|
+
click.echo(f" 📌 {name}")
|
|
108
|
+
click.echo(f" {health} {total} calls {bar} {success_rate:.0%} avg {avg_latency:.0f}ms")
|
|
109
|
+
if failed:
|
|
110
|
+
click.echo(f" ❌ {failed} failures: {', '.join(f'{k}({v})' for k, v in errors.items())}")
|
|
111
|
+
click.echo()
|
|
112
|
+
else:
|
|
113
|
+
click.echo(f"\n📊 Stats: {name}")
|
|
114
|
+
click.echo(f"{'━' * 45}")
|
|
115
|
+
click.echo(f" Health: {health}")
|
|
116
|
+
click.echo(f" Total calls: {total}")
|
|
117
|
+
click.echo(f" Success rate: {success_rate:.1%}")
|
|
118
|
+
click.echo(f" Failed calls: {failed}")
|
|
119
|
+
click.echo(f"{'━' * 45}")
|
|
120
|
+
click.echo(f" Avg latency: {avg_latency:.1f} ms")
|
|
121
|
+
click.echo(f" P50 latency: {p50:.1f} ms")
|
|
122
|
+
click.echo(f" P95 latency: {p95:.1f} ms")
|
|
123
|
+
click.echo(f" P99 latency: {p99:.1f} ms")
|
|
124
|
+
if errors:
|
|
125
|
+
click.echo(f"{'━' * 45}")
|
|
126
|
+
click.echo(f" Errors:")
|
|
127
|
+
for err_type, count in errors.items():
|
|
128
|
+
click.echo(f" {err_type}: {count}")
|
|
129
|
+
click.echo(f"{'━' * 45}")
|
|
130
|
+
click.echo(f" First seen: {first}")
|
|
131
|
+
click.echo(f" Last seen: {last}")
|
|
132
|
+
click.echo()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --- Search Command ---
|
|
136
|
+
|
|
137
|
+
@cli.command()
|
|
138
|
+
@click.argument("query")
|
|
139
|
+
@click.option("--language", "-l", help="Filter by language (e.g., en, zh)")
|
|
140
|
+
@click.option("--max-price", "-p", type=float, help="Maximum price per unit")
|
|
141
|
+
@click.option("--min-trust", "-t", type=float, help="Minimum trust score (0-100)")
|
|
142
|
+
@click.option("--verified", is_flag=True, help="Only show verified agents")
|
|
143
|
+
@click.option("--protocol", type=click.Choice(["a2a", "mcp", "any"]), default="any")
|
|
144
|
+
@click.option("--limit", "-n", default=10, help="Number of results")
|
|
145
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
146
|
+
def search(query, language, max_price, min_trust, verified, protocol, limit, output_json):
|
|
147
|
+
"""Search for agents by capability."""
|
|
148
|
+
from agentdna.discovery import search_agents
|
|
149
|
+
|
|
150
|
+
results = search_agents(
|
|
151
|
+
skill=query,
|
|
152
|
+
language=language,
|
|
153
|
+
max_price=max_price,
|
|
154
|
+
min_reputation=min_trust,
|
|
155
|
+
verified=verified,
|
|
156
|
+
protocol=protocol if protocol != "any" else None,
|
|
157
|
+
limit=limit,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if output_json:
|
|
161
|
+
import json
|
|
162
|
+
from dataclasses import asdict
|
|
163
|
+
click.echo(json.dumps([asdict(a) for a in results.agents], indent=2, default=str))
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if not results.agents:
|
|
167
|
+
click.echo(f"No agents found for: {query}")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
click.echo(f"\n🧬 Found {results.total} agents for '{query}':\n")
|
|
171
|
+
for agent in results.agents:
|
|
172
|
+
score = agent.trust_score.total if agent.trust_score else "?"
|
|
173
|
+
verified_badge = " 🏆" if agent.verified else ""
|
|
174
|
+
click.echo(f" {agent.name} v{agent.version} (DNA: {score}/100){verified_badge}")
|
|
175
|
+
click.echo(f" ID: {agent.id}")
|
|
176
|
+
click.echo(f" {agent.description}")
|
|
177
|
+
for cap in agent.capabilities:
|
|
178
|
+
price = cap.pricing.display() if cap.pricing else "N/A"
|
|
179
|
+
langs = ", ".join(cap.languages) if cap.languages else "any"
|
|
180
|
+
click.echo(f" → {cap.skill} ({price}) [{langs}]")
|
|
181
|
+
click.echo()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --- Register Command ---
|
|
185
|
+
|
|
186
|
+
@cli.command()
|
|
187
|
+
@click.argument("path", default="./agentdna.yaml")
|
|
188
|
+
def register(path):
|
|
189
|
+
"""Register your agent in the AgentDNA registry."""
|
|
190
|
+
from agentdna.registry import register_agent
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
result = register_agent(path)
|
|
194
|
+
click.echo(f"✅ Registered: {result.get('agent_id', 'success')}")
|
|
195
|
+
except FileNotFoundError:
|
|
196
|
+
click.echo(f"❌ File not found: {path}")
|
|
197
|
+
click.echo("Run 'agentdna init' to create one.")
|
|
198
|
+
sys.exit(1)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
click.echo(f"❌ Registration failed: {e}")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# --- Init Command ---
|
|
205
|
+
|
|
206
|
+
@cli.command()
|
|
207
|
+
@click.argument("name", default="MyAgent")
|
|
208
|
+
@click.option("--output", "-o", default="./agentdna.yaml", help="Output file path")
|
|
209
|
+
def init(name, output):
|
|
210
|
+
"""Generate a starter agentdna.yaml file."""
|
|
211
|
+
from agentdna.registry import generate_agent_card
|
|
212
|
+
|
|
213
|
+
path = generate_agent_card(name=name, output_path=output)
|
|
214
|
+
click.echo(f"✅ Created agent card: {path}")
|
|
215
|
+
click.echo("Edit it with your agent's details, then run: agentdna register")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# --- Trust Command ---
|
|
219
|
+
|
|
220
|
+
@cli.command()
|
|
221
|
+
@click.argument("agent_id")
|
|
222
|
+
def trust(agent_id):
|
|
223
|
+
"""View the trust score for an agent."""
|
|
224
|
+
from agentdna.client import AgentDNAClient
|
|
225
|
+
|
|
226
|
+
with AgentDNAClient() as client:
|
|
227
|
+
score = client.get_trust_score(agent_id)
|
|
228
|
+
|
|
229
|
+
click.echo(f"\n🧬 Trust Score: {agent_id}\n")
|
|
230
|
+
click.echo(f" Total: {score.get('total', 0)}/100")
|
|
231
|
+
click.echo(f" Task Completion: {score.get('task_completion', 0)}/40")
|
|
232
|
+
click.echo(f" Response Quality: {score.get('response_quality', 0)}/25")
|
|
233
|
+
click.echo(f" Latency Reliability: {score.get('latency_reliability', 0)}/15")
|
|
234
|
+
click.echo(f" Uptime: {score.get('uptime_score', 0)}/10")
|
|
235
|
+
click.echo(f" Verification: {score.get('verification_bonus', 0)}/10")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# --- Status Command ---
|
|
239
|
+
|
|
240
|
+
@cli.command()
|
|
241
|
+
@click.argument("agent_id")
|
|
242
|
+
def status(agent_id):
|
|
243
|
+
"""Check agent health status."""
|
|
244
|
+
from agentdna.client import AgentDNAClient
|
|
245
|
+
|
|
246
|
+
with AgentDNAClient() as client:
|
|
247
|
+
info = client.get_agent(agent_id)
|
|
248
|
+
|
|
249
|
+
click.echo(f"\n🧬 Agent Status: {agent_id}\n")
|
|
250
|
+
click.echo(f" Name: {info.get('name', 'Unknown')}")
|
|
251
|
+
click.echo(f" Version: {info.get('version', 'Unknown')}")
|
|
252
|
+
click.echo(f" Status: {'🟢 Online' if info.get('online') else '🔴 Offline'}")
|
|
253
|
+
click.echo(f" Uptime: {info.get('uptime', 'Unknown')}")
|
|
254
|
+
click.echo(f" Tasks: {info.get('total_tasks_completed', 0)} completed")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# --- Review Command ---
|
|
258
|
+
|
|
259
|
+
@cli.command()
|
|
260
|
+
@click.argument("agent_id")
|
|
261
|
+
@click.option("--rating", "-r", type=click.IntRange(1, 5), prompt="Rating (1-5)")
|
|
262
|
+
@click.option("--comment", "-c", prompt="Review comment")
|
|
263
|
+
def review(agent_id, rating, comment):
|
|
264
|
+
"""Submit a review for an agent."""
|
|
265
|
+
from agentdna.client import AgentDNAClient
|
|
266
|
+
|
|
267
|
+
with AgentDNAClient() as client:
|
|
268
|
+
client.submit_review(agent_id, rating, comment)
|
|
269
|
+
|
|
270
|
+
click.echo(f"✅ Review submitted for {agent_id}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
cli()
|
agentdna/client.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""AgentDNA API client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
AGENTDNA_API_URL = os.environ.get("AGENTDNA_API_URL", "https://api.agentdna.dev")
|
|
9
|
+
AGENTDNA_API_KEY = os.environ.get("AGENTDNA_API_KEY", "")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentDNAClient:
|
|
13
|
+
"""Client for the AgentDNA registry API."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_url: str = AGENTDNA_API_URL,
|
|
18
|
+
api_key: str = AGENTDNA_API_KEY,
|
|
19
|
+
):
|
|
20
|
+
self.api_url = api_url.rstrip("/")
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self._client = httpx.Client(
|
|
23
|
+
base_url=self.api_url,
|
|
24
|
+
headers={
|
|
25
|
+
"Authorization": f"Bearer {self.api_key}" if self.api_key else "",
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"User-Agent": "agentdna-python/0.1.0",
|
|
28
|
+
},
|
|
29
|
+
timeout=30.0,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _get(self, path: str, params: Optional[dict] = None) -> dict:
|
|
33
|
+
resp = self._client.get(path, params=params)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
return resp.json()
|
|
36
|
+
|
|
37
|
+
def _post(self, path: str, payload: Optional[dict] = None) -> dict:
|
|
38
|
+
resp = self._client.post(path, json=payload)
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
return resp.json()
|
|
41
|
+
|
|
42
|
+
# --- Registry ---
|
|
43
|
+
|
|
44
|
+
def register(self, agent_card: dict) -> dict:
|
|
45
|
+
"""Register an agent in the registry."""
|
|
46
|
+
return self._post("/api/v1/agents", payload=agent_card)
|
|
47
|
+
|
|
48
|
+
def get_agent(self, agent_id: str) -> dict:
|
|
49
|
+
"""Get agent details by DNA ID."""
|
|
50
|
+
return self._get(f"/api/v1/agents/{agent_id}")
|
|
51
|
+
|
|
52
|
+
def list_agents(self, limit: int = 20, offset: int = 0) -> dict:
|
|
53
|
+
"""List registered agents."""
|
|
54
|
+
return self._get("/api/v1/agents", params={"limit": limit, "offset": offset})
|
|
55
|
+
|
|
56
|
+
# --- Discovery ---
|
|
57
|
+
|
|
58
|
+
def search(
|
|
59
|
+
self,
|
|
60
|
+
skill: Optional[str] = None,
|
|
61
|
+
language: Optional[str] = None,
|
|
62
|
+
max_price: Optional[float] = None,
|
|
63
|
+
min_reputation: Optional[float] = None,
|
|
64
|
+
verified: Optional[bool] = None,
|
|
65
|
+
protocol: Optional[str] = None,
|
|
66
|
+
tags: Optional[list[str]] = None,
|
|
67
|
+
limit: int = 10,
|
|
68
|
+
offset: int = 0,
|
|
69
|
+
) -> dict:
|
|
70
|
+
"""Search for agents by capability."""
|
|
71
|
+
params: dict[str, str | int | float] = {"limit": limit, "offset": offset}
|
|
72
|
+
if skill:
|
|
73
|
+
params["skill"] = skill
|
|
74
|
+
if language:
|
|
75
|
+
params["language"] = language
|
|
76
|
+
if max_price is not None:
|
|
77
|
+
params["max_price"] = max_price
|
|
78
|
+
if min_reputation is not None:
|
|
79
|
+
params["min_reputation"] = min_reputation
|
|
80
|
+
if verified is not None:
|
|
81
|
+
params["verified"] = str(verified).lower()
|
|
82
|
+
if protocol:
|
|
83
|
+
params["protocol"] = protocol
|
|
84
|
+
if tags:
|
|
85
|
+
params["tags"] = ",".join(tags)
|
|
86
|
+
return self._get("/api/v1/agents/search", params=params)
|
|
87
|
+
|
|
88
|
+
# --- Trust ---
|
|
89
|
+
|
|
90
|
+
def get_trust_score(self, agent_id: str) -> dict:
|
|
91
|
+
"""Get the trust/reputation score for an agent."""
|
|
92
|
+
return self._get(f"/api/v1/agents/{agent_id}/trust")
|
|
93
|
+
|
|
94
|
+
def submit_review(self, agent_id: str, rating: int, comment: str, task_id: Optional[str] = None) -> dict:
|
|
95
|
+
"""Submit a review for an agent."""
|
|
96
|
+
return self._post(
|
|
97
|
+
f"/api/v1/agents/{agent_id}/reviews",
|
|
98
|
+
payload={"rating": rating, "comment": comment, "task_id": task_id},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# --- Marketplace ---
|
|
102
|
+
|
|
103
|
+
def create_task(self, agent_id: str, task: dict) -> dict:
|
|
104
|
+
"""Create a task for an agent (hire them)."""
|
|
105
|
+
return self._post(f"/api/v1/agents/{agent_id}/tasks", payload=task)
|
|
106
|
+
|
|
107
|
+
def get_task(self, task_id: str) -> dict:
|
|
108
|
+
"""Get task status and result."""
|
|
109
|
+
return self._get(f"/api/v1/tasks/{task_id}")
|
|
110
|
+
|
|
111
|
+
# --- Heartbeat ---
|
|
112
|
+
|
|
113
|
+
def ping(self, agent_id: str) -> dict:
|
|
114
|
+
"""Send a heartbeat ping for an agent."""
|
|
115
|
+
return self._post(f"/api/v1/agents/{agent_id}/heartbeat")
|
|
116
|
+
|
|
117
|
+
def close(self):
|
|
118
|
+
"""Close the HTTP client."""
|
|
119
|
+
self._client.close()
|
|
120
|
+
|
|
121
|
+
def __enter__(self):
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, *args):
|
|
125
|
+
self.close()
|
agentdna/discovery.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Agent discovery and search."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from agentdna.client import AgentDNAClient
|
|
6
|
+
from agentdna.models import Agent, AgentSearchResult, Capability, Pricing, TrustScore
|
|
7
|
+
|
|
8
|
+
# Fields accepted by TrustScore dataclass (ignore extra keys from server)
|
|
9
|
+
_TRUST_FIELDS = {"total", "task_completion", "response_quality",
|
|
10
|
+
"latency_reliability", "uptime_score", "verification_bonus"}
|
|
11
|
+
# Fields accepted by Pricing dataclass
|
|
12
|
+
_PRICING_FIELDS = {"model", "amount", "currency", "free_tier"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_agent(data: dict) -> Agent:
|
|
16
|
+
"""Parse API response into Agent model."""
|
|
17
|
+
capabilities = []
|
|
18
|
+
for cap in data.get("capabilities", []) or []:
|
|
19
|
+
if not cap:
|
|
20
|
+
continue
|
|
21
|
+
pricing = None
|
|
22
|
+
if cap.get("pricing"):
|
|
23
|
+
pricing_data = {k: v for k, v in cap["pricing"].items() if k in _PRICING_FIELDS}
|
|
24
|
+
pricing = Pricing(**pricing_data)
|
|
25
|
+
capabilities.append(
|
|
26
|
+
Capability(
|
|
27
|
+
skill=cap.get("skill", ""),
|
|
28
|
+
description=cap.get("description", ""),
|
|
29
|
+
inputs=cap.get("inputs", []),
|
|
30
|
+
output=cap.get("output", ""),
|
|
31
|
+
languages=cap.get("languages", []),
|
|
32
|
+
pricing=pricing,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
trust = None
|
|
37
|
+
if data.get("trust_score"):
|
|
38
|
+
ts_data = {k: v for k, v in data["trust_score"].items() if k in _TRUST_FIELDS}
|
|
39
|
+
trust = TrustScore(**ts_data)
|
|
40
|
+
elif data.get("trust_total") is not None:
|
|
41
|
+
# Server returns trust fields with trust_ prefix (flat structure)
|
|
42
|
+
trust = TrustScore(
|
|
43
|
+
total=data.get("trust_total", 0),
|
|
44
|
+
task_completion=data.get("trust_task_completion", 0),
|
|
45
|
+
response_quality=data.get("trust_response_quality", 0),
|
|
46
|
+
latency_reliability=data.get("trust_latency_reliability", 0),
|
|
47
|
+
uptime_score=data.get("trust_uptime_score", 0),
|
|
48
|
+
verification_bonus=data.get("trust_verification_bonus", 0),
|
|
49
|
+
)
|
|
50
|
+
elif (
|
|
51
|
+
isinstance(data.get("total"), int)
|
|
52
|
+
and "task_completion" in data
|
|
53
|
+
):
|
|
54
|
+
# Flat trust fields (e.g., direct trust endpoint response)
|
|
55
|
+
trust = TrustScore(
|
|
56
|
+
total=data.get("total", 0),
|
|
57
|
+
task_completion=data.get("task_completion", 0),
|
|
58
|
+
response_quality=data.get("response_quality", 0),
|
|
59
|
+
latency_reliability=data.get("latency_reliability", 0),
|
|
60
|
+
uptime_score=data.get("uptime_score", 0),
|
|
61
|
+
verification_bonus=data.get("verification_bonus", 0),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Safe nested access — handles None values at any level
|
|
65
|
+
metadata = data.get("metadata") or {}
|
|
66
|
+
owner = data.get("owner") or {}
|
|
67
|
+
|
|
68
|
+
return Agent(
|
|
69
|
+
id=data.get("id", data.get("agent_id", "")),
|
|
70
|
+
name=data.get("name", ""),
|
|
71
|
+
version=data.get("version", ""),
|
|
72
|
+
description=data.get("description", ""),
|
|
73
|
+
protocol=data.get("protocol", ""),
|
|
74
|
+
endpoint=data.get("endpoint", ""),
|
|
75
|
+
capabilities=capabilities,
|
|
76
|
+
trust_score=trust,
|
|
77
|
+
tags=metadata.get("tags", []),
|
|
78
|
+
verified=data.get("verified", False),
|
|
79
|
+
owner_name=owner.get("name", ""),
|
|
80
|
+
repository=metadata.get("repository", ""),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def find_agent(
|
|
85
|
+
skill: Optional[str] = None,
|
|
86
|
+
language: Optional[str] = None,
|
|
87
|
+
max_price: Optional[float] = None,
|
|
88
|
+
min_reputation: Optional[float] = None,
|
|
89
|
+
verified: Optional[bool] = None,
|
|
90
|
+
protocol: Optional[str] = None,
|
|
91
|
+
tags: Optional[list[str]] = None,
|
|
92
|
+
api_key: Optional[str] = None,
|
|
93
|
+
) -> Optional[Agent]:
|
|
94
|
+
"""
|
|
95
|
+
Find the best agent matching your criteria.
|
|
96
|
+
|
|
97
|
+
Returns the highest-reputation agent that matches all filters.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
agent = find_agent(
|
|
101
|
+
skill="transcribe",
|
|
102
|
+
language="zh",
|
|
103
|
+
max_price=0.03,
|
|
104
|
+
min_reputation=4.5,
|
|
105
|
+
verified=True
|
|
106
|
+
)
|
|
107
|
+
"""
|
|
108
|
+
results = search_agents(
|
|
109
|
+
skill=skill,
|
|
110
|
+
language=language,
|
|
111
|
+
max_price=max_price,
|
|
112
|
+
min_reputation=min_reputation,
|
|
113
|
+
verified=verified,
|
|
114
|
+
protocol=protocol,
|
|
115
|
+
tags=tags,
|
|
116
|
+
limit=1,
|
|
117
|
+
api_key=api_key,
|
|
118
|
+
)
|
|
119
|
+
return results.best()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def search_agents(
|
|
123
|
+
skill: Optional[str] = None,
|
|
124
|
+
language: Optional[str] = None,
|
|
125
|
+
max_price: Optional[float] = None,
|
|
126
|
+
min_reputation: Optional[float] = None,
|
|
127
|
+
verified: Optional[bool] = None,
|
|
128
|
+
protocol: Optional[str] = None,
|
|
129
|
+
tags: Optional[list[str]] = None,
|
|
130
|
+
limit: int = 10,
|
|
131
|
+
offset: int = 0,
|
|
132
|
+
api_key: Optional[str] = None,
|
|
133
|
+
) -> AgentSearchResult:
|
|
134
|
+
"""
|
|
135
|
+
Search for agents by capability.
|
|
136
|
+
|
|
137
|
+
Returns a list of matching agents sorted by trust score.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
results = search_agents(skill="code-review", language="en")
|
|
141
|
+
for agent in results.agents:
|
|
142
|
+
print(f"{agent.name}: {agent.trust_score.total}/100")
|
|
143
|
+
"""
|
|
144
|
+
with AgentDNAClient(api_key=api_key or "") as client:
|
|
145
|
+
data = client.search(
|
|
146
|
+
skill=skill,
|
|
147
|
+
language=language,
|
|
148
|
+
max_price=max_price,
|
|
149
|
+
min_reputation=min_reputation,
|
|
150
|
+
verified=verified,
|
|
151
|
+
protocol=protocol,
|
|
152
|
+
tags=tags,
|
|
153
|
+
limit=limit,
|
|
154
|
+
offset=offset,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
agents = [_parse_agent(a) for a in data.get("agents", [])]
|
|
158
|
+
return AgentSearchResult(
|
|
159
|
+
agents=agents,
|
|
160
|
+
total=data.get("total", len(agents)),
|
|
161
|
+
query=data.get("query", {}),
|
|
162
|
+
)
|