schemarouter 0.2.0a1__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.
- schemarouter/__init__.py +88 -0
- schemarouter/_version.py +8 -0
- schemarouter/adapters/__init__.py +29 -0
- schemarouter/adapters/base.py +71 -0
- schemarouter/adapters/mcp.py +179 -0
- schemarouter/adapters/openapi.py +418 -0
- schemarouter/adapters/optimade.py +656 -0
- schemarouter/adapters/python.py +188 -0
- schemarouter/analyzers/__init__.py +3 -0
- schemarouter/analyzers/model.py +185 -0
- schemarouter/errors.py +50 -0
- schemarouter/executor.py +207 -0
- schemarouter/ingestion.py +348 -0
- schemarouter/integrations/__init__.py +3 -0
- schemarouter/integrations/langchain.py +96 -0
- schemarouter/models.py +154 -0
- schemarouter/planner.py +253 -0
- schemarouter/policy.py +51 -0
- schemarouter/proposals.py +391 -0
- schemarouter/py.typed +0 -0
- schemarouter/registry.py +84 -0
- schemarouter/runs.py +77 -0
- schemarouter/runtime.py +686 -0
- schemarouter/validation.py +96 -0
- schemarouter-0.2.0a1.dist-info/METADATA +297 -0
- schemarouter-0.2.0a1.dist-info/RECORD +28 -0
- schemarouter-0.2.0a1.dist-info/WHEEL +4 -0
- schemarouter-0.2.0a1.dist-info/licenses/LICENSE +21 -0
schemarouter/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
from .adapters.base import AdapterContext, AdapterLoadResult, AdapterRegistry, SourceAdapter
|
|
3
|
+
from .adapters.optimade import OPTIMADESourceAdapter
|
|
4
|
+
from .adapters.python import schema_tool, tool_from_callable
|
|
5
|
+
from .analyzers import ModelCallable, ModelQueryAnalyzer
|
|
6
|
+
from .errors import (
|
|
7
|
+
BindingDriftError,
|
|
8
|
+
ExecutionError,
|
|
9
|
+
ModelAnalysisError,
|
|
10
|
+
PlanningError,
|
|
11
|
+
PlanValidationError,
|
|
12
|
+
PolicyViolationError,
|
|
13
|
+
ProposalApprovalError,
|
|
14
|
+
RegistrationError,
|
|
15
|
+
SchemaDriftError,
|
|
16
|
+
SchemaRouterError,
|
|
17
|
+
SchemaSourceError,
|
|
18
|
+
SchemaValidationError,
|
|
19
|
+
UnsupportedSchemaSourceError,
|
|
20
|
+
)
|
|
21
|
+
from .executor import RegistryExecutor
|
|
22
|
+
from .models import (
|
|
23
|
+
EndpointSpec,
|
|
24
|
+
EvidenceRequirements,
|
|
25
|
+
ExecutionPlan,
|
|
26
|
+
FieldSpec,
|
|
27
|
+
ParameterSpec,
|
|
28
|
+
PlanRequest,
|
|
29
|
+
QueryIntent,
|
|
30
|
+
ToolCall,
|
|
31
|
+
ToolResult,
|
|
32
|
+
ToolSpec,
|
|
33
|
+
)
|
|
34
|
+
from .planner import KeywordAnalyzer, QueryAnalyzer, SchemaPlanner
|
|
35
|
+
from .policy import ExecutionPolicy
|
|
36
|
+
from .proposals import SchemaProposal
|
|
37
|
+
from .registry import InMemoryRegistry, ToolRegistry
|
|
38
|
+
from .runs import RetryPolicy, RunConfig, RunEvent
|
|
39
|
+
from .runtime import ConfiguredSchemaRouter, SchemaRouter
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"__version__",
|
|
43
|
+
"AdapterContext",
|
|
44
|
+
"AdapterLoadResult",
|
|
45
|
+
"AdapterRegistry",
|
|
46
|
+
"BindingDriftError",
|
|
47
|
+
"ConfiguredSchemaRouter",
|
|
48
|
+
"EndpointSpec",
|
|
49
|
+
"EvidenceRequirements",
|
|
50
|
+
"ExecutionError",
|
|
51
|
+
"ExecutionPlan",
|
|
52
|
+
"ExecutionPolicy",
|
|
53
|
+
"FieldSpec",
|
|
54
|
+
"InMemoryRegistry",
|
|
55
|
+
"KeywordAnalyzer",
|
|
56
|
+
"ModelAnalysisError",
|
|
57
|
+
"ModelCallable",
|
|
58
|
+
"ModelQueryAnalyzer",
|
|
59
|
+
"ParameterSpec",
|
|
60
|
+
"PlanRequest",
|
|
61
|
+
"PlanValidationError",
|
|
62
|
+
"PlanningError",
|
|
63
|
+
"PolicyViolationError",
|
|
64
|
+
"ProposalApprovalError",
|
|
65
|
+
"QueryAnalyzer",
|
|
66
|
+
"QueryIntent",
|
|
67
|
+
"RegistrationError",
|
|
68
|
+
"RegistryExecutor",
|
|
69
|
+
"RetryPolicy",
|
|
70
|
+
"RunConfig",
|
|
71
|
+
"RunEvent",
|
|
72
|
+
"SchemaDriftError",
|
|
73
|
+
"SchemaPlanner",
|
|
74
|
+
"SchemaProposal",
|
|
75
|
+
"SchemaRouter",
|
|
76
|
+
"SchemaRouterError",
|
|
77
|
+
"SchemaSourceError",
|
|
78
|
+
"SchemaValidationError",
|
|
79
|
+
"SourceAdapter",
|
|
80
|
+
"OPTIMADESourceAdapter",
|
|
81
|
+
"ToolCall",
|
|
82
|
+
"ToolRegistry",
|
|
83
|
+
"ToolResult",
|
|
84
|
+
"ToolSpec",
|
|
85
|
+
"UnsupportedSchemaSourceError",
|
|
86
|
+
"schema_tool",
|
|
87
|
+
"tool_from_callable",
|
|
88
|
+
]
|
schemarouter/_version.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .base import AdapterContext, AdapterLoadResult, AdapterRegistry, SourceAdapter
|
|
2
|
+
from .mcp import MCPRemoteInvoker, inspect_mcp_url, tool_from_mcp
|
|
3
|
+
from .openapi import OpenAPIRemoteInvoker, resolve_openapi_base_url, tool_from_openapi
|
|
4
|
+
from .optimade import OPTIMADERemoteInvoker, OPTIMADESourceAdapter
|
|
5
|
+
from .python import (
|
|
6
|
+
PythonCallableInvoker,
|
|
7
|
+
callable_options,
|
|
8
|
+
schema_tool,
|
|
9
|
+
tool_from_callable,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AdapterContext",
|
|
14
|
+
"AdapterLoadResult",
|
|
15
|
+
"AdapterRegistry",
|
|
16
|
+
"MCPRemoteInvoker",
|
|
17
|
+
"OPTIMADERemoteInvoker",
|
|
18
|
+
"OPTIMADESourceAdapter",
|
|
19
|
+
"OpenAPIRemoteInvoker",
|
|
20
|
+
"PythonCallableInvoker",
|
|
21
|
+
"SourceAdapter",
|
|
22
|
+
"callable_options",
|
|
23
|
+
"inspect_mcp_url",
|
|
24
|
+
"resolve_openapi_base_url",
|
|
25
|
+
"schema_tool",
|
|
26
|
+
"tool_from_callable",
|
|
27
|
+
"tool_from_mcp",
|
|
28
|
+
"tool_from_openapi",
|
|
29
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from ..models import ToolSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class AdapterContext:
|
|
13
|
+
url: str
|
|
14
|
+
name: str | None = None
|
|
15
|
+
namespace: str | None = None
|
|
16
|
+
base_url: str | None = None
|
|
17
|
+
schema_headers: dict[str, str] | None = None
|
|
18
|
+
trusted_headers: dict[str, str] | None = None
|
|
19
|
+
timeout: float = 20.0
|
|
20
|
+
http_client: httpx.AsyncClient | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AdapterLoadResult:
|
|
25
|
+
tool: ToolSpec
|
|
26
|
+
invoker: Any | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SourceAdapter(Protocol):
|
|
30
|
+
kind: str
|
|
31
|
+
priority: int
|
|
32
|
+
|
|
33
|
+
async def load(self, context: AdapterContext) -> AdapterLoadResult | None: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdapterRegistry:
|
|
37
|
+
"""Ordered registry for structured capability-source adapters."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, adapters: list[SourceAdapter] | None = None) -> None:
|
|
40
|
+
self._adapters: dict[str, SourceAdapter] = {}
|
|
41
|
+
for adapter in adapters or []:
|
|
42
|
+
self.register(adapter)
|
|
43
|
+
|
|
44
|
+
def register(self, adapter: SourceAdapter, *, replace: bool = False) -> None:
|
|
45
|
+
kind = str(adapter.kind).strip().lower()
|
|
46
|
+
if not kind or kind == "auto":
|
|
47
|
+
raise ValueError("adapter kind must be a non-empty name other than 'auto'")
|
|
48
|
+
if kind in self._adapters and not replace:
|
|
49
|
+
raise ValueError(f"adapter kind already registered: {kind!r}")
|
|
50
|
+
self._adapters[kind] = adapter
|
|
51
|
+
|
|
52
|
+
def unregister(self, kind: str) -> None:
|
|
53
|
+
self._adapters.pop(kind.strip().lower(), None)
|
|
54
|
+
|
|
55
|
+
def get(self, kind: str) -> SourceAdapter:
|
|
56
|
+
normalized = kind.strip().lower()
|
|
57
|
+
try:
|
|
58
|
+
return self._adapters[normalized]
|
|
59
|
+
except KeyError as exc:
|
|
60
|
+
raise KeyError(f"unknown adapter kind: {normalized!r}") from exc
|
|
61
|
+
|
|
62
|
+
def kinds(self) -> tuple[str, ...]:
|
|
63
|
+
return tuple(sorted(self._adapters))
|
|
64
|
+
|
|
65
|
+
def ordered(self) -> tuple[SourceAdapter, ...]:
|
|
66
|
+
return tuple(
|
|
67
|
+
sorted(
|
|
68
|
+
self._adapters.values(),
|
|
69
|
+
key=lambda adapter: (-int(adapter.priority), str(adapter.kind)),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..errors import SchemaSourceError
|
|
6
|
+
from ..models import EndpointSpec, FieldSpec, ParameterSpec, ToolSpec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _properties(schema: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
|
|
10
|
+
if not isinstance(schema, dict):
|
|
11
|
+
return {}
|
|
12
|
+
props = schema.get("properties", {})
|
|
13
|
+
return props if isinstance(props, dict) else {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
17
|
+
if isinstance(value, dict):
|
|
18
|
+
return value
|
|
19
|
+
if hasattr(value, "model_dump"):
|
|
20
|
+
return value.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
21
|
+
raise TypeError(f"cannot convert MCP value {type(value)!r} to dict")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def tool_from_mcp(
|
|
25
|
+
server_name: str,
|
|
26
|
+
tools_list_result: dict[str, Any] | list[dict[str, Any]],
|
|
27
|
+
*,
|
|
28
|
+
namespace: str | None = None,
|
|
29
|
+
) -> ToolSpec:
|
|
30
|
+
"""Convert an MCP tools/list result into a single server-scoped ToolSpec.
|
|
31
|
+
|
|
32
|
+
Remote annotations are preserved as untrusted metadata only; they do not grant permissions.
|
|
33
|
+
"""
|
|
34
|
+
raw_tools = (
|
|
35
|
+
tools_list_result.get("tools", [])
|
|
36
|
+
if isinstance(tools_list_result, dict)
|
|
37
|
+
else tools_list_result
|
|
38
|
+
)
|
|
39
|
+
endpoints: list[EndpointSpec] = []
|
|
40
|
+
for raw_item in raw_tools:
|
|
41
|
+
item = _as_dict(raw_item)
|
|
42
|
+
input_schema = item.get("inputSchema") or item.get("input_schema") or {}
|
|
43
|
+
required = (
|
|
44
|
+
set(input_schema.get("required", []))
|
|
45
|
+
if isinstance(input_schema, dict)
|
|
46
|
+
else set()
|
|
47
|
+
)
|
|
48
|
+
parameters = [
|
|
49
|
+
ParameterSpec(
|
|
50
|
+
name=name,
|
|
51
|
+
description=spec.get("description", "") if isinstance(spec, dict) else "",
|
|
52
|
+
required=name in required,
|
|
53
|
+
location="argument",
|
|
54
|
+
json_schema=spec if isinstance(spec, dict) else {},
|
|
55
|
+
)
|
|
56
|
+
for name, spec in _properties(input_schema).items()
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
output_schema = item.get("outputSchema") or item.get("output_schema") or {}
|
|
60
|
+
output_required = (
|
|
61
|
+
set(output_schema.get("required", []))
|
|
62
|
+
if isinstance(output_schema, dict)
|
|
63
|
+
else set()
|
|
64
|
+
)
|
|
65
|
+
fields = [
|
|
66
|
+
FieldSpec(
|
|
67
|
+
name=name,
|
|
68
|
+
description=spec.get("description", "") if isinstance(spec, dict) else "",
|
|
69
|
+
json_schema=spec if isinstance(spec, dict) else {},
|
|
70
|
+
identifier=name in {"id", "uuid", "key"} or name.endswith("_id"),
|
|
71
|
+
aliases=[name.replace("_", " ")],
|
|
72
|
+
)
|
|
73
|
+
for name, spec in _properties(output_schema).items()
|
|
74
|
+
]
|
|
75
|
+
metadata = {
|
|
76
|
+
"title": item.get("title"),
|
|
77
|
+
"annotations": item.get("annotations"),
|
|
78
|
+
"output_required": sorted(output_required),
|
|
79
|
+
"remote_name": item.get("name"),
|
|
80
|
+
}
|
|
81
|
+
endpoints.append(
|
|
82
|
+
EndpointSpec(
|
|
83
|
+
name=item["name"],
|
|
84
|
+
description=item.get("description", ""),
|
|
85
|
+
parameters=parameters,
|
|
86
|
+
output_fields=fields,
|
|
87
|
+
input_schema=input_schema if isinstance(input_schema, dict) else {},
|
|
88
|
+
output_schema=output_schema if isinstance(output_schema, dict) else {},
|
|
89
|
+
metadata=metadata,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return ToolSpec(
|
|
94
|
+
name=server_name,
|
|
95
|
+
namespace=namespace,
|
|
96
|
+
description=f"MCP server: {server_name}",
|
|
97
|
+
endpoints=endpoints,
|
|
98
|
+
metadata={"adapter": "mcp", "remote_metadata_untrusted": True},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def inspect_mcp_url(
|
|
103
|
+
url: str,
|
|
104
|
+
*,
|
|
105
|
+
server_name: str | None = None,
|
|
106
|
+
namespace: str | None = None,
|
|
107
|
+
) -> ToolSpec:
|
|
108
|
+
"""Connect to a Streamable HTTP MCP URL and import all advertised tools."""
|
|
109
|
+
try:
|
|
110
|
+
from mcp import Client
|
|
111
|
+
except ImportError as exc:
|
|
112
|
+
raise SchemaSourceError(
|
|
113
|
+
'MCP support requires the optional dependency: pip install "schemarouter[mcp]"'
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
raw_tools: list[dict[str, Any]] = []
|
|
117
|
+
try:
|
|
118
|
+
async with Client(url) as client:
|
|
119
|
+
cursor: str | None = None
|
|
120
|
+
while True:
|
|
121
|
+
page = await client.list_tools(cursor=cursor)
|
|
122
|
+
raw_tools.extend(_as_dict(tool) for tool in page.tools)
|
|
123
|
+
cursor = page.next_cursor
|
|
124
|
+
if cursor is None:
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
info = getattr(client, "server_info", None)
|
|
128
|
+
discovered_name = getattr(info, "name", None)
|
|
129
|
+
protocol_version = getattr(client, "protocol_version", None)
|
|
130
|
+
except Exception as exc: # noqa: BLE001
|
|
131
|
+
raise SchemaSourceError(f"failed to inspect MCP server at {url!r}") from exc
|
|
132
|
+
|
|
133
|
+
name = server_name or discovered_name or "mcp_server"
|
|
134
|
+
tool = tool_from_mcp(name, raw_tools, namespace=namespace)
|
|
135
|
+
tool.metadata.update(
|
|
136
|
+
{
|
|
137
|
+
"source_url": url,
|
|
138
|
+
"protocol_version": protocol_version,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
return tool
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MCPRemoteInvoker:
|
|
145
|
+
"""Trusted runtime adapter for a remote MCP server.
|
|
146
|
+
|
|
147
|
+
A fresh client lifecycle is used per invocation in v0.1. Connection pooling belongs in a
|
|
148
|
+
later transport layer so the core executor remains stateless and easy to reason about.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, url: str) -> None:
|
|
152
|
+
self.url = url
|
|
153
|
+
|
|
154
|
+
async def __call__(self, endpoint: str, arguments: dict[str, Any]) -> Any:
|
|
155
|
+
try:
|
|
156
|
+
from mcp import Client
|
|
157
|
+
except ImportError as exc:
|
|
158
|
+
raise SchemaSourceError(
|
|
159
|
+
'MCP execution requires: pip install "schemarouter[mcp]"'
|
|
160
|
+
) from exc
|
|
161
|
+
|
|
162
|
+
async with Client(self.url) as client:
|
|
163
|
+
result = await client.call_tool(endpoint, arguments)
|
|
164
|
+
if getattr(result, "is_error", False):
|
|
165
|
+
raise RuntimeError(f"MCP tool {endpoint!r} returned an error")
|
|
166
|
+
|
|
167
|
+
structured = getattr(result, "structured_content", None)
|
|
168
|
+
if structured is not None:
|
|
169
|
+
return structured
|
|
170
|
+
|
|
171
|
+
content = getattr(result, "content", None)
|
|
172
|
+
if content is None:
|
|
173
|
+
return None
|
|
174
|
+
return [
|
|
175
|
+
block.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
176
|
+
if hasattr(block, "model_dump")
|
|
177
|
+
else str(block)
|
|
178
|
+
for block in content
|
|
179
|
+
]
|