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.
- opensmartroute/__init__.py +209 -0
- opensmartroute/adapters/__init__.py +162 -0
- opensmartroute/adapters/a2a.py +246 -0
- opensmartroute/adapters/catalogue.py +596 -0
- opensmartroute/adapters/frameworks.py +200 -0
- opensmartroute/adapters/handlers.py +260 -0
- opensmartroute/adapters/harness.py +266 -0
- opensmartroute/adapters/mcp.py +438 -0
- opensmartroute/adapters/mcp_servers.py +136 -0
- opensmartroute/adapters/openai_compat.py +247 -0
- opensmartroute/adapters/optional.py +171 -0
- opensmartroute/adapters/personas.py +175 -0
- opensmartroute/adapters/semantic_router.py +182 -0
- opensmartroute/adapters/skills.py +183 -0
- opensmartroute/adapters/websearch.py +425 -0
- opensmartroute/aio.py +49 -0
- opensmartroute/branding.py +131 -0
- opensmartroute/cli.py +1656 -0
- opensmartroute/config.py +85 -0
- opensmartroute/core/__init__.py +37 -0
- opensmartroute/core/registry.py +97 -0
- opensmartroute/core/types.py +342 -0
- opensmartroute/credentials.py +426 -0
- opensmartroute/discovery.py +363 -0
- opensmartroute/enterprise/__init__.py +1053 -0
- opensmartroute/enterprise/ops.py +463 -0
- opensmartroute/enterprise/savings.py +296 -0
- opensmartroute/enterprise/stores.py +368 -0
- opensmartroute/errors.py +86 -0
- opensmartroute/estimate.py +299 -0
- opensmartroute/eval/__init__.py +295 -0
- opensmartroute/eval/agentic.py +304 -0
- opensmartroute/eval/audit.py +400 -0
- opensmartroute/eval/baselines.py +236 -0
- opensmartroute/eval/collect.py +833 -0
- opensmartroute/eval/criteria.py +840 -0
- opensmartroute/eval/datasets.py +246 -0
- opensmartroute/eval/frontier.py +119 -0
- opensmartroute/eval/headroom.py +222 -0
- opensmartroute/eval/ope.py +169 -0
- opensmartroute/eval/robustness.py +219 -0
- opensmartroute/execution.py +456 -0
- opensmartroute/feedback/__init__.py +80 -0
- opensmartroute/learning/__init__.py +599 -0
- opensmartroute/learning/attention.py +258 -0
- opensmartroute/learning/autopilot.py +259 -0
- opensmartroute/learning/coldstart.py +356 -0
- opensmartroute/learning/contrastive.py +555 -0
- opensmartroute/learning/credit.py +150 -0
- opensmartroute/learning/embed.py +101 -0
- opensmartroute/learning/handoff.py +209 -0
- opensmartroute/learning/multiturn.py +224 -0
- opensmartroute/learning/personal.py +221 -0
- opensmartroute/learning/policy_gradient.py +228 -0
- opensmartroute/learning/self_improve.py +321 -0
- opensmartroute/learning/slm.py +546 -0
- opensmartroute/math/__init__.py +103 -0
- opensmartroute/math/bandits.py +621 -0
- opensmartroute/math/calibration.py +256 -0
- opensmartroute/math/decision.py +118 -0
- opensmartroute/math/dirichlet.py +194 -0
- opensmartroute/math/energy.py +166 -0
- opensmartroute/math/estimators.py +181 -0
- opensmartroute/math/irt.py +147 -0
- opensmartroute/math/markov.py +199 -0
- opensmartroute/math/preference.py +121 -0
- opensmartroute/mcp_server.py +648 -0
- opensmartroute/observability.py +787 -0
- opensmartroute/ocm.py +367 -0
- opensmartroute/policy/__init__.py +277 -0
- opensmartroute/py.typed +0 -0
- opensmartroute/realtime/__init__.py +281 -0
- opensmartroute/retrieval.py +440 -0
- opensmartroute/router.py +732 -0
- opensmartroute/sdk.py +642 -0
- opensmartroute/security/__init__.py +342 -0
- opensmartroute/security/gadget.py +462 -0
- opensmartroute/security/injection.py +180 -0
- opensmartroute/security/limits.py +266 -0
- opensmartroute/security/provenance.py +280 -0
- opensmartroute/security/safety.py +219 -0
- opensmartroute/server.py +636 -0
- opensmartroute/settings.py +303 -0
- opensmartroute/signals/__init__.py +741 -0
- opensmartroute/signals/events.py +222 -0
- opensmartroute/signals/models.py +476 -0
- opensmartroute/signals/ontology.py +292 -0
- opensmartroute/signals/uncertainty.py +390 -0
- opensmartroute/stack.py +517 -0
- opensmartroute/strategies/__init__.py +116 -0
- opensmartroute/strategies/aggregate.py +193 -0
- opensmartroute/strategies/auction.py +163 -0
- opensmartroute/strategies/bandit.py +114 -0
- opensmartroute/strategies/base.py +39 -0
- opensmartroute/strategies/capability.py +101 -0
- opensmartroute/strategies/cascade.py +262 -0
- opensmartroute/strategies/defer.py +144 -0
- opensmartroute/strategies/edge.py +101 -0
- opensmartroute/strategies/elastic.py +100 -0
- opensmartroute/strategies/escalation.py +164 -0
- opensmartroute/strategies/human.py +293 -0
- opensmartroute/strategies/llm_judge.py +170 -0
- opensmartroute/strategies/memory.py +271 -0
- opensmartroute/strategies/modality.py +219 -0
- opensmartroute/strategies/probe.py +82 -0
- opensmartroute/strategies/progress.py +458 -0
- opensmartroute/strategies/protocol.py +153 -0
- opensmartroute/strategies/rules.py +133 -0
- opensmartroute/strategies/semantic_cache.py +224 -0
- opensmartroute/strategies/session.py +185 -0
- opensmartroute/strategies/similarity.py +91 -0
- opensmartroute/strategies/speculative.py +279 -0
- opensmartroute/strategies/task_table.py +172 -0
- opensmartroute-0.5.0.dist-info/METADATA +402 -0
- opensmartroute-0.5.0.dist-info/RECORD +118 -0
- opensmartroute-0.5.0.dist-info/WHEEL +4 -0
- opensmartroute-0.5.0.dist-info/entry_points.txt +2 -0
- 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"]
|