opensmartroute 0.5.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.
Files changed (118) hide show
  1. opensmartroute/__init__.py +209 -0
  2. opensmartroute/adapters/__init__.py +162 -0
  3. opensmartroute/adapters/a2a.py +246 -0
  4. opensmartroute/adapters/catalogue.py +596 -0
  5. opensmartroute/adapters/frameworks.py +200 -0
  6. opensmartroute/adapters/handlers.py +260 -0
  7. opensmartroute/adapters/harness.py +266 -0
  8. opensmartroute/adapters/mcp.py +438 -0
  9. opensmartroute/adapters/mcp_servers.py +136 -0
  10. opensmartroute/adapters/openai_compat.py +247 -0
  11. opensmartroute/adapters/optional.py +171 -0
  12. opensmartroute/adapters/personas.py +175 -0
  13. opensmartroute/adapters/semantic_router.py +182 -0
  14. opensmartroute/adapters/skills.py +183 -0
  15. opensmartroute/adapters/websearch.py +425 -0
  16. opensmartroute/aio.py +49 -0
  17. opensmartroute/branding.py +131 -0
  18. opensmartroute/cli.py +1656 -0
  19. opensmartroute/config.py +85 -0
  20. opensmartroute/core/__init__.py +37 -0
  21. opensmartroute/core/registry.py +97 -0
  22. opensmartroute/core/types.py +342 -0
  23. opensmartroute/credentials.py +426 -0
  24. opensmartroute/discovery.py +363 -0
  25. opensmartroute/enterprise/__init__.py +1053 -0
  26. opensmartroute/enterprise/ops.py +463 -0
  27. opensmartroute/enterprise/savings.py +296 -0
  28. opensmartroute/enterprise/stores.py +368 -0
  29. opensmartroute/errors.py +86 -0
  30. opensmartroute/estimate.py +299 -0
  31. opensmartroute/eval/__init__.py +295 -0
  32. opensmartroute/eval/agentic.py +304 -0
  33. opensmartroute/eval/audit.py +400 -0
  34. opensmartroute/eval/baselines.py +236 -0
  35. opensmartroute/eval/collect.py +833 -0
  36. opensmartroute/eval/criteria.py +840 -0
  37. opensmartroute/eval/datasets.py +246 -0
  38. opensmartroute/eval/frontier.py +119 -0
  39. opensmartroute/eval/headroom.py +222 -0
  40. opensmartroute/eval/ope.py +169 -0
  41. opensmartroute/eval/robustness.py +219 -0
  42. opensmartroute/execution.py +456 -0
  43. opensmartroute/feedback/__init__.py +80 -0
  44. opensmartroute/learning/__init__.py +599 -0
  45. opensmartroute/learning/attention.py +258 -0
  46. opensmartroute/learning/autopilot.py +259 -0
  47. opensmartroute/learning/coldstart.py +356 -0
  48. opensmartroute/learning/contrastive.py +555 -0
  49. opensmartroute/learning/credit.py +150 -0
  50. opensmartroute/learning/embed.py +101 -0
  51. opensmartroute/learning/handoff.py +209 -0
  52. opensmartroute/learning/multiturn.py +224 -0
  53. opensmartroute/learning/personal.py +221 -0
  54. opensmartroute/learning/policy_gradient.py +228 -0
  55. opensmartroute/learning/self_improve.py +321 -0
  56. opensmartroute/learning/slm.py +546 -0
  57. opensmartroute/math/__init__.py +103 -0
  58. opensmartroute/math/bandits.py +621 -0
  59. opensmartroute/math/calibration.py +256 -0
  60. opensmartroute/math/decision.py +118 -0
  61. opensmartroute/math/dirichlet.py +194 -0
  62. opensmartroute/math/energy.py +166 -0
  63. opensmartroute/math/estimators.py +181 -0
  64. opensmartroute/math/irt.py +147 -0
  65. opensmartroute/math/markov.py +199 -0
  66. opensmartroute/math/preference.py +121 -0
  67. opensmartroute/mcp_server.py +648 -0
  68. opensmartroute/observability.py +787 -0
  69. opensmartroute/ocm.py +367 -0
  70. opensmartroute/policy/__init__.py +277 -0
  71. opensmartroute/py.typed +0 -0
  72. opensmartroute/realtime/__init__.py +281 -0
  73. opensmartroute/retrieval.py +440 -0
  74. opensmartroute/router.py +732 -0
  75. opensmartroute/sdk.py +642 -0
  76. opensmartroute/security/__init__.py +342 -0
  77. opensmartroute/security/gadget.py +462 -0
  78. opensmartroute/security/injection.py +180 -0
  79. opensmartroute/security/limits.py +266 -0
  80. opensmartroute/security/provenance.py +280 -0
  81. opensmartroute/security/safety.py +219 -0
  82. opensmartroute/server.py +636 -0
  83. opensmartroute/settings.py +303 -0
  84. opensmartroute/signals/__init__.py +741 -0
  85. opensmartroute/signals/events.py +222 -0
  86. opensmartroute/signals/models.py +476 -0
  87. opensmartroute/signals/ontology.py +292 -0
  88. opensmartroute/signals/uncertainty.py +390 -0
  89. opensmartroute/stack.py +517 -0
  90. opensmartroute/strategies/__init__.py +116 -0
  91. opensmartroute/strategies/aggregate.py +193 -0
  92. opensmartroute/strategies/auction.py +163 -0
  93. opensmartroute/strategies/bandit.py +114 -0
  94. opensmartroute/strategies/base.py +39 -0
  95. opensmartroute/strategies/capability.py +101 -0
  96. opensmartroute/strategies/cascade.py +262 -0
  97. opensmartroute/strategies/defer.py +144 -0
  98. opensmartroute/strategies/edge.py +101 -0
  99. opensmartroute/strategies/elastic.py +100 -0
  100. opensmartroute/strategies/escalation.py +164 -0
  101. opensmartroute/strategies/human.py +293 -0
  102. opensmartroute/strategies/llm_judge.py +170 -0
  103. opensmartroute/strategies/memory.py +271 -0
  104. opensmartroute/strategies/modality.py +219 -0
  105. opensmartroute/strategies/probe.py +82 -0
  106. opensmartroute/strategies/progress.py +458 -0
  107. opensmartroute/strategies/protocol.py +153 -0
  108. opensmartroute/strategies/rules.py +133 -0
  109. opensmartroute/strategies/semantic_cache.py +224 -0
  110. opensmartroute/strategies/session.py +185 -0
  111. opensmartroute/strategies/similarity.py +91 -0
  112. opensmartroute/strategies/speculative.py +279 -0
  113. opensmartroute/strategies/task_table.py +172 -0
  114. opensmartroute-0.5.0.dist-info/METADATA +402 -0
  115. opensmartroute-0.5.0.dist-info/RECORD +118 -0
  116. opensmartroute-0.5.0.dist-info/WHEEL +4 -0
  117. opensmartroute-0.5.0.dist-info/entry_points.txt +2 -0
  118. opensmartroute-0.5.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,209 @@
1
+ """OpenSmartRoute — an open, intelligent route to the right decision, solution, or destination.
2
+
3
+ Public SDK surface. Anything importable from this module is covered by semantic
4
+ versioning; sub-modules may change between minor versions.
5
+
6
+ Quick start::
7
+
8
+ from opensmartroute import Router, TargetRegistry, RouteTarget, TargetKind, Capabilities
9
+
10
+ reg = TargetRegistry([
11
+ RouteTarget("gpt-small", TargetKind.LLM, capabilities=Capabilities(max_complexity=0.5),
12
+ cost={"usd_per_1k_tokens": 0.0002}),
13
+ RouteTarget("gpt-large", TargetKind.LLM, capabilities=Capabilities(min_complexity=0.4),
14
+ cost={"usd_per_1k_tokens": 0.01}, quality_prior=0.9),
15
+ ])
16
+ router = Router(reg)
17
+ decision = router.route("Prove that sqrt(2) is irrational, step by step.")
18
+ print(decision.target.id, decision.confidence)
19
+ print(decision.trace.explain())
20
+
21
+ Decorator SDK (see :mod:`opensmartroute.sdk`)::
22
+
23
+ import opensmartroute as osr
24
+
25
+ @osr.tool("weather", domains=["travel"], actions=["lookup"])
26
+ def weather(request, **kw):
27
+ \"\"\"Current weather for a city.\"\"\"
28
+ return lookup(request.text)
29
+
30
+ @osr.strategy(weight=0.8)
31
+ def geo_affinity(request, signals, candidates):
32
+ return {t.id: 1.0 if request.constraints.region in t.constraints.regions else 0.4 for t in candidates}
33
+
34
+ @osr.policy_rule
35
+ def business_hours(target, request, signals):
36
+ return "closed" if target.kind is osr.TargetKind.HUMAN and request.context.get("after_hours") else None
37
+
38
+ router = osr.components.router(reg) # or osr.components.builder(reg).with_auto_learning().build()
39
+
40
+ Settings (all tunables, ``OSR_<GROUP>_<FIELD>`` environment overrides)::
41
+
42
+ from opensmartroute import Settings, configure
43
+ from opensmartroute.settings import PolicySettings
44
+ configure(Settings().replace(policy=PolicySettings(jailbreak_threshold=0.7)))
45
+
46
+ Tracing and observability (see :mod:`opensmartroute.observability`)::
47
+
48
+ from opensmartroute import configure_tracing
49
+ from opensmartroute.observability import MemorySink, MetricsSink
50
+
51
+ tracer = configure_tracing(MemorySink(), MetricsSink()) # or configure_tracing() -> OSR_OBSERVABILITY_*
52
+ d = router.route("...")
53
+ tracer.find(MemorySink).trace(d.request_id) # spans + events of that request
54
+
55
+ Enterprise::
56
+
57
+ from opensmartroute.enterprise import RouterBuilder, LoggingTelemetry, CacheMiddleware
58
+ from opensmartroute.security import GuardMiddleware
59
+
60
+ app = (RouterBuilder(reg)
61
+ .with_defaults()
62
+ .with_components() # everything declared with the decorators above
63
+ .with_auto_learning(state_dir=".osr-state")
64
+ .with_health(latency_slo_ms=3000)
65
+ .with_middleware(GuardMiddleware(redact=True), CacheMiddleware())
66
+ .with_telemetry(LoggingTelemetry())
67
+ .build())
68
+ """
69
+
70
+ from .aio import AsyncRouter
71
+ from .config import load_rules, load_targets
72
+ from .core import (
73
+ Capabilities,
74
+ Objective,
75
+ Outcome,
76
+ PlanSlot,
77
+ RankedTarget,
78
+ RequestConstraints,
79
+ RouteDecision,
80
+ RoutePlan,
81
+ RouteRequest,
82
+ RouteTarget,
83
+ RouteTrace,
84
+ Signals,
85
+ StrategyScore,
86
+ TargetConstraints,
87
+ TargetKind,
88
+ TargetRegistry,
89
+ )
90
+ from .errors import (
91
+ ConfigurationError,
92
+ ExecutionError,
93
+ NoRouteError,
94
+ OpenSmartRouteError,
95
+ SecurityError,
96
+ StateStoreError,
97
+ TargetUnavailableError,
98
+ ValidationError,
99
+ )
100
+ from .execution import ExecutionResult, ExecutionStep
101
+ from .feedback import FeedbackStore
102
+ from .learning.self_improve import SelfImprover
103
+ from .learning.slm import RouterSLM, SLMStrategy
104
+ from .observability import Event, EventSink, Span, Tracer, configure_tracing, get_tracer
105
+ from .policy import Policy
106
+ from .router import Router
107
+ from .sdk import (
108
+ ComponentRegistry,
109
+ agent,
110
+ components,
111
+ middleware,
112
+ policy_rule,
113
+ signal,
114
+ skill,
115
+ strategy,
116
+ target,
117
+ telemetry,
118
+ tool,
119
+ )
120
+ from .settings import Settings, configure, get_settings
121
+ from .strategies import (
122
+ BanditStrategy,
123
+ CapabilityStrategy,
124
+ Cascade,
125
+ CascadePlanner,
126
+ DeferStrategy,
127
+ EffortStrategy,
128
+ LLMJudgeStrategy,
129
+ ProgressRouter,
130
+ Rule,
131
+ RulesStrategy,
132
+ SimilarityStrategy,
133
+ Strategy,
134
+ TaskTableStrategy,
135
+ )
136
+
137
+ __version__ = "0.5.0"
138
+
139
+ __all__ = [
140
+ "AsyncRouter",
141
+ "BanditStrategy",
142
+ "Capabilities",
143
+ "CapabilityStrategy",
144
+ "Cascade",
145
+ "CascadePlanner",
146
+ "ComponentRegistry",
147
+ "ConfigurationError",
148
+ "DeferStrategy",
149
+ "EffortStrategy",
150
+ "Event",
151
+ "EventSink",
152
+ "ExecutionError",
153
+ "ExecutionResult",
154
+ "ExecutionStep",
155
+ "FeedbackStore",
156
+ "LLMJudgeStrategy",
157
+ "NoRouteError",
158
+ "Objective",
159
+ "OpenSmartRouteError",
160
+ "Outcome",
161
+ "PlanSlot",
162
+ "Policy",
163
+ "ProgressRouter",
164
+ "RankedTarget",
165
+ "RequestConstraints",
166
+ "RouteDecision",
167
+ "RoutePlan",
168
+ "RouteRequest",
169
+ "RouteTarget",
170
+ "RouteTrace",
171
+ "Router",
172
+ "RouterSLM",
173
+ "Rule",
174
+ "RulesStrategy",
175
+ "SLMStrategy",
176
+ "SecurityError",
177
+ "SelfImprover",
178
+ "Settings",
179
+ "Signals",
180
+ "SimilarityStrategy",
181
+ "Span",
182
+ "StateStoreError",
183
+ "Strategy",
184
+ "StrategyScore",
185
+ "TargetConstraints",
186
+ "TargetKind",
187
+ "TargetRegistry",
188
+ "TargetUnavailableError",
189
+ "TaskTableStrategy",
190
+ "Tracer",
191
+ "ValidationError",
192
+ "__version__",
193
+ "agent",
194
+ "components",
195
+ "configure",
196
+ "configure_tracing",
197
+ "get_settings",
198
+ "get_tracer",
199
+ "load_rules",
200
+ "load_targets",
201
+ "middleware",
202
+ "policy_rule",
203
+ "signal",
204
+ "skill",
205
+ "strategy",
206
+ "target",
207
+ "telemetry",
208
+ "tool",
209
+ ]
@@ -0,0 +1,162 @@
1
+ """Adapters connect OpenSmartRoute to real providers and infrastructure.
2
+
3
+ * :mod:`openai_compat` — stdlib HTTP client for any OpenAI-compatible API
4
+ (OpenAI, Azure OpenAI, vLLM, Ollama, LiteLLM, OpenRouter, Groq, Mistral …) with
5
+ glue for the LLM judge, the similarity embedder, and target handlers.
6
+ * :mod:`optional` — sentence-transformers embedder and OpenTelemetry telemetry,
7
+ lazily imported so the core stays dependency-free.
8
+ * :mod:`harness` — agent-harness adapters (callable, HTTP, subprocess CLI) so a
9
+ coding / research / support runtime is a routable ``TargetKind.AGENT``.
10
+ * :mod:`skills` — load Agent-Skills ``SKILL.md`` packages as ``TargetKind.SKILL``.
11
+ * :mod:`mcp` — MCP ``tools/list`` import (+ stdio client), description enrichment,
12
+ HMAC / Ed25519 signed manifests.
13
+ * :mod:`a2a` — A2A Agent Cards as ``TargetKind.AGENT`` (+ per-skill slot targets).
14
+ * :mod:`mcp_servers` — rank MCP *servers* for a task before connecting (server cards).
15
+ * :mod:`semantic_router` — import a vLLM semantic-router ``config.yaml`` as targets + rules.
16
+ * :mod:`personas` — persona catalogues (markdown dirs, JSON/CSV, ``*.agent.md``).
17
+ * :mod:`frameworks` — LangGraph node / Agent Framework executor / OpenAI tool spec.
18
+ * :mod:`handlers` — executors for non-LLM targets: HTTP endpoints, MCP tools, async queues
19
+ (humans, ticketing, long-running workflows) returning a pending ticket.
20
+ * :mod:`catalogue` — live model catalogue (OpenRouter prices, Hugging Face cards) → targets,
21
+ cost/quality benchmark table and Pareto frontier.
22
+ * :mod:`websearch` — bounded https fetch, HTML→text, keyless search providers (Hugging Face,
23
+ DuckDuckGo; Brave with a key) behind :class:`WebKnowledge` for the self-improving SLM.
24
+ """
25
+
26
+ from .a2a import a2a_handler, agent_from_card, fetch_agent_card, skills_from_card
27
+ from .catalogue import (
28
+ ModelCard,
29
+ ModelCatalogue,
30
+ card_to_target,
31
+ fetch_huggingface_models,
32
+ fetch_leaderboard_quality,
33
+ fetch_openrouter_models,
34
+ model_key,
35
+ quality_from_benchmarks,
36
+ quality_from_popularity,
37
+ )
38
+ from .frameworks import (
39
+ langgraph_condition,
40
+ langgraph_node,
41
+ maf_router_executor,
42
+ openai_tool_handler,
43
+ openai_tool_spec,
44
+ route_and_execute,
45
+ )
46
+ from .handlers import (
47
+ InMemoryQueue,
48
+ PendingResult,
49
+ Queue,
50
+ QueuedItem,
51
+ http_handler,
52
+ mcp_tool_handler,
53
+ queue_handler,
54
+ )
55
+ from .harness import (
56
+ AgentHarness,
57
+ CallableHarness,
58
+ HarnessResult,
59
+ HTTPHarness,
60
+ SubprocessHarness,
61
+ harness_handler,
62
+ )
63
+ from .mcp import (
64
+ StdioMCPClient,
65
+ connect_mcp,
66
+ enrich_description,
67
+ manifest_from_targets,
68
+ sign_manifest,
69
+ tools_from_manifest,
70
+ tools_from_mcp,
71
+ verify_manifest,
72
+ )
73
+ from .mcp_servers import ServerCard, ServerRecommendation, recommend_servers
74
+ from .openai_compat import ChatResult, OpenAICompatClient, attach_chat_handlers, chat_handler, embedder, judge_fn
75
+ from .optional import OpenTelemetrySink, OpenTelemetryTelemetry, sentence_transformers_embedder
76
+ from .personas import load_personas, persona_from_markdown, persona_target, personas_from_records
77
+ from .semantic_router import SemanticRouterImport, load_semantic_router_config
78
+ from .skills import load_skill, load_skills, skill_from_markdown
79
+ from .websearch import (
80
+ SearchHit,
81
+ WebKnowledge,
82
+ brave_search,
83
+ duckduckgo_search,
84
+ fetch_bytes,
85
+ fetch_json,
86
+ fetch_page_text,
87
+ html_to_text,
88
+ huggingface_search,
89
+ )
90
+
91
+ __all__ = [
92
+ "AgentHarness",
93
+ "CallableHarness",
94
+ "ChatResult",
95
+ "HTTPHarness",
96
+ "HarnessResult",
97
+ "InMemoryQueue",
98
+ "ModelCard",
99
+ "ModelCatalogue",
100
+ "OpenAICompatClient",
101
+ "OpenTelemetrySink",
102
+ "OpenTelemetryTelemetry",
103
+ "PendingResult",
104
+ "Queue",
105
+ "QueuedItem",
106
+ "SearchHit",
107
+ "SemanticRouterImport",
108
+ "ServerCard",
109
+ "ServerRecommendation",
110
+ "StdioMCPClient",
111
+ "SubprocessHarness",
112
+ "WebKnowledge",
113
+ "a2a_handler",
114
+ "agent_from_card",
115
+ "brave_search",
116
+ "card_to_target",
117
+ "attach_chat_handlers",
118
+ "chat_handler",
119
+ "connect_mcp",
120
+ "duckduckgo_search",
121
+ "embedder",
122
+ "enrich_description",
123
+ "fetch_agent_card",
124
+ "fetch_bytes",
125
+ "fetch_huggingface_models",
126
+ "fetch_json",
127
+ "fetch_leaderboard_quality",
128
+ "fetch_openrouter_models",
129
+ "fetch_page_text",
130
+ "harness_handler",
131
+ "html_to_text",
132
+ "http_handler",
133
+ "huggingface_search",
134
+ "judge_fn",
135
+ "langgraph_condition",
136
+ "langgraph_node",
137
+ "load_personas",
138
+ "load_semantic_router_config",
139
+ "load_skill",
140
+ "load_skills",
141
+ "maf_router_executor",
142
+ "manifest_from_targets",
143
+ "model_key",
144
+ "mcp_tool_handler",
145
+ "openai_tool_handler",
146
+ "openai_tool_spec",
147
+ "persona_from_markdown",
148
+ "persona_target",
149
+ "personas_from_records",
150
+ "quality_from_benchmarks",
151
+ "quality_from_popularity",
152
+ "queue_handler",
153
+ "recommend_servers",
154
+ "route_and_execute",
155
+ "sentence_transformers_embedder",
156
+ "sign_manifest",
157
+ "skill_from_markdown",
158
+ "skills_from_card",
159
+ "tools_from_manifest",
160
+ "tools_from_mcp",
161
+ "verify_manifest",
162
+ ]
@@ -0,0 +1,246 @@
1
+ """Import A2A (Agent-to-Agent protocol) **Agent Cards** as ``TargetKind.AGENT`` targets.
2
+
3
+ An Agent Card (served at ``/.well-known/agent.json`` / ``agent-card.json``) advertises an
4
+ agent's ``name``, ``description``, ``url``, ``skills`` (each with ``id``, ``name``,
5
+ ``description``, ``tags``, ``examples``, ``inputModes``/``outputModes``), ``capabilities``
6
+ (``streaming``, ``pushNotifications``) and ``defaultInputModes``/``defaultOutputModes``.
7
+
8
+ * :func:`agent_from_card` – one target per agent, whose routing text/examples are the union of
9
+ its skills (so the agent is found for any of them).
10
+ * :func:`skills_from_card` – optionally one ``TargetKind.SKILL`` target per advertised skill,
11
+ ``primary=False`` and linked to the agent through ``metadata["agent_id"]``; lets the planner
12
+ pick a specific skill and the executor pass ``skillId`` to the agent.
13
+ * :func:`a2a_handler` – stdlib JSON-RPC ``message/send`` client (``tasks/send`` for older
14
+ servers) so the imported agent is executable; extracts the text parts of the final
15
+ artifact / status message.
16
+ * :func:`fetch_agent_card` – GET the well-known card over https.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import time
23
+ import urllib.error
24
+ import urllib.request
25
+ import uuid
26
+ from collections.abc import Callable
27
+ from typing import Any
28
+
29
+ from ..core.types import Capabilities, RouteRequest, RouteTarget, TargetKind
30
+ from ..errors import ConfigurationError, ExecutionError, TargetUnavailableError
31
+
32
+ _MODALITY = {
33
+ "text": "text",
34
+ "text/plain": "text",
35
+ "application/json": "text",
36
+ "image": "image",
37
+ "image/png": "image",
38
+ "image/jpeg": "image",
39
+ "audio": "audio",
40
+ "audio/wav": "audio",
41
+ "audio/mpeg": "audio",
42
+ "video": "video",
43
+ "video/mp4": "video",
44
+ "file": "file",
45
+ }
46
+
47
+
48
+ def _modes(values: Any) -> list[str]:
49
+ out: list[str] = []
50
+ for v in values or []:
51
+ m = _MODALITY.get(str(v).lower(), str(v).split("/")[0].lower())
52
+ if m not in out:
53
+ out.append(m)
54
+ return out or ["text"]
55
+
56
+
57
+ def _agent_id(card: dict[str, Any]) -> str:
58
+ name = str(card.get("name", "agent")).strip().lower()
59
+ slug = "".join(c if c.isalnum() else "-" for c in name).strip("-")
60
+ return f"a2a:{slug or 'agent'}"
61
+
62
+
63
+ def agent_from_card(
64
+ card: dict[str, Any],
65
+ *,
66
+ call: Callable[[str, RouteRequest], Any] | None = None,
67
+ cost_per_1k_tokens: float = 0.0,
68
+ latency_ms: float = 2000.0,
69
+ quality_prior: float = 0.65,
70
+ languages: list[str] | None = None,
71
+ ) -> RouteTarget:
72
+ """Build an ``agent`` RouteTarget from an A2A agent card (name, skills, tags, input modes)."""
73
+ if not isinstance(card, dict) or not card.get("name"):
74
+ raise ConfigurationError("agent card must be a mapping with a name")
75
+ skills = [s for s in card.get("skills", []) if isinstance(s, dict)]
76
+ examples: list[str] = []
77
+ tags: list[str] = ["a2a"]
78
+ domains: list[str] = []
79
+ actions: list[str] = []
80
+ for s in skills:
81
+ examples += [str(e) for e in s.get("examples", []) if isinstance(e, str)]
82
+ d = s.get("description")
83
+ if isinstance(d, str) and d:
84
+ examples.append(d)
85
+ for t in s.get("tags", []) or []:
86
+ t = str(t).lower()
87
+ tags.append(t)
88
+ domains.append(t)
89
+ sid = str(s.get("id", "")).lower()
90
+ if sid:
91
+ actions.append(sid)
92
+ modalities = _modes(card.get("defaultInputModes")) + [m for s in skills for m in _modes(s.get("inputModes"))]
93
+ caps = Capabilities(
94
+ domains=list(dict.fromkeys(domains)),
95
+ actions=list(dict.fromkeys(actions)),
96
+ languages=languages or ["en"],
97
+ modalities=list(dict.fromkeys(modalities)),
98
+ tags=list(dict.fromkeys(tags)),
99
+ supports_tools=True,
100
+ supports_streaming=bool((card.get("capabilities") or {}).get("streaming", False)),
101
+ )
102
+ url = str(card.get("url", ""))
103
+ handler = None
104
+ if call is not None:
105
+ agent_url = url
106
+
107
+ def _h(request: RouteRequest, **_: Any) -> Any:
108
+ return call(agent_url, request)
109
+
110
+ handler = _h
111
+ elif url.startswith("https://"):
112
+ handler = a2a_handler(url)
113
+ return RouteTarget(
114
+ id=_agent_id(card),
115
+ kind=TargetKind.AGENT,
116
+ name=str(card["name"]),
117
+ description=str(card.get("description", "")),
118
+ capabilities=caps,
119
+ cost={"usd_per_1k_tokens": cost_per_1k_tokens},
120
+ latency_ms=latency_ms,
121
+ quality_prior=quality_prior,
122
+ examples=examples[:24],
123
+ handler=handler,
124
+ metadata={
125
+ "source": "a2a",
126
+ "url": url,
127
+ "version": card.get("version"),
128
+ "provider": card.get("provider"),
129
+ "skills": [str(s.get("id", "")) for s in skills],
130
+ "capabilities": card.get("capabilities", {}),
131
+ "security_schemes": list((card.get("securitySchemes") or {}).keys()),
132
+ },
133
+ )
134
+
135
+
136
+ def skills_from_card(card: dict[str, Any], agent_id: str | None = None) -> list[RouteTarget]:
137
+ """One non-primary ``skill`` target per skill declared on an A2A agent card (``<agent>/<skill>``)."""
138
+ aid = agent_id or _agent_id(card)
139
+ out: list[RouteTarget] = []
140
+ for s in card.get("skills", []) or []:
141
+ if not isinstance(s, dict) or not s.get("id"):
142
+ continue
143
+ tags = [str(t).lower() for t in s.get("tags", []) or []]
144
+ out.append(
145
+ RouteTarget(
146
+ id=f"{aid}/{s['id']}",
147
+ kind=TargetKind.SKILL,
148
+ name=str(s.get("name", s["id"])),
149
+ description=str(s.get("description", "")),
150
+ capabilities=Capabilities(
151
+ domains=tags, tags=["a2a-skill", *tags], modalities=_modes(s.get("inputModes"))
152
+ ),
153
+ quality_prior=0.65,
154
+ examples=[str(e) for e in s.get("examples", []) if isinstance(e, str)],
155
+ primary=False,
156
+ metadata={"source": "a2a", "agent_id": aid, "skill_id": str(s["id"])},
157
+ )
158
+ )
159
+ return out
160
+
161
+
162
+ def fetch_agent_card(base_url: str, timeout_s: float = 10.0, path: str = "/.well-known/agent.json") -> dict[str, Any]:
163
+ """Download an agent card over HTTPS (plain HTTP is refused); raises TargetUnavailableError on failure."""
164
+ if not base_url.startswith("https://"):
165
+ raise ConfigurationError("agent card url must be https")
166
+ url = base_url.rstrip("/") + path
167
+ req = urllib.request.Request(url, headers={"Accept": "application/json"}) # noqa: S310 - https enforced
168
+ try:
169
+ with urllib.request.urlopen(req, timeout=timeout_s) as resp: # noqa: S310 # nosec B310 - https enforced above
170
+ data = json.loads(resp.read().decode())
171
+ except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e:
172
+ raise TargetUnavailableError(
173
+ "could not fetch agent card", details={"url": url, "error": type(e).__name__}
174
+ ) from None
175
+ if not isinstance(data, dict):
176
+ raise ConfigurationError("agent card is not a JSON object")
177
+ return data
178
+
179
+
180
+ def _extract_text(result: Any) -> str:
181
+ texts: list[str] = []
182
+
183
+ def walk(o: Any) -> None:
184
+ if isinstance(o, dict):
185
+ if o.get("kind") == "text" or o.get("type") == "text":
186
+ t = o.get("text")
187
+ if isinstance(t, str):
188
+ texts.append(t)
189
+ for k in ("artifacts", "parts", "status", "message", "history", "result"):
190
+ if k in o:
191
+ walk(o[k])
192
+ elif isinstance(o, list):
193
+ for x in o:
194
+ walk(x)
195
+
196
+ walk(result)
197
+ return "\n".join(texts)
198
+
199
+
200
+ def a2a_handler(
201
+ url: str,
202
+ token: str | None = None,
203
+ timeout_s: float = 60.0,
204
+ method: str = "message/send",
205
+ ) -> Callable[..., Any]:
206
+ """Return a handler that sends the request text to an A2A agent and returns its text."""
207
+ if not url.startswith(("https://", "http://localhost", "http://127.0.0.1")):
208
+ raise ConfigurationError("a2a url must be https (or localhost)")
209
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
210
+ if token:
211
+ headers["Authorization"] = f"Bearer {token}"
212
+
213
+ def _h(request: RouteRequest, **_: Any) -> Any:
214
+ msg: dict[str, Any] = {
215
+ "role": "user",
216
+ "parts": [{"kind": "text", "text": request.text}],
217
+ "messageId": uuid.uuid4().hex,
218
+ }
219
+ skill = request.context.get("skill_id")
220
+ params: dict[str, Any] = {"message": msg}
221
+ if isinstance(skill, str) and skill:
222
+ params["metadata"] = {"skillId": skill}
223
+ body = json.dumps({"jsonrpc": "2.0", "id": uuid.uuid4().hex, "method": method, "params": params}).encode()
224
+ req = urllib.request.Request(url, data=body, headers=headers, method="POST") # noqa: S310
225
+ t0 = time.perf_counter()
226
+ try:
227
+ with urllib.request.urlopen(req, timeout=timeout_s) as resp: # noqa: S310 # nosec B310 - scheme validated
228
+ data = json.loads(resp.read().decode())
229
+ except urllib.error.HTTPError as e:
230
+ raise TargetUnavailableError(f"a2a upstream {e.code}", details={"url": url}) from None
231
+ except (urllib.error.URLError, TimeoutError) as e:
232
+ raise TargetUnavailableError(
233
+ "a2a connection failed", details={"url": url, "error": type(e).__name__}
234
+ ) from None
235
+ if "error" in data:
236
+ raise ExecutionError("a2a error", details=data["error"])
237
+ return {
238
+ "text": _extract_text(data.get("result")),
239
+ "raw": data.get("result"),
240
+ "latency_ms": (time.perf_counter() - t0) * 1000,
241
+ }
242
+
243
+ return _h
244
+
245
+
246
+ __all__ = ["a2a_handler", "agent_from_card", "fetch_agent_card", "skills_from_card"]