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 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
+ )