toolbound 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.
- toolbound/__init__.py +123 -0
- toolbound/adapters/__init__.py +9 -0
- toolbound/adapters/_common.py +84 -0
- toolbound/adapters/anthropic.py +81 -0
- toolbound/adapters/openai.py +115 -0
- toolbound/cli.py +69 -0
- toolbound/core/__init__.py +4 -0
- toolbound/core/approval.py +314 -0
- toolbound/core/budget.py +42 -0
- toolbound/core/context.py +56 -0
- toolbound/core/contracts.py +689 -0
- toolbound/core/credentials.py +41 -0
- toolbound/core/errors.py +117 -0
- toolbound/core/exposure.py +68 -0
- toolbound/core/gateway.py +1461 -0
- toolbound/core/network.py +267 -0
- toolbound/core/observe.py +60 -0
- toolbound/core/paths.py +138 -0
- toolbound/core/ports.py +166 -0
- toolbound/core/pyschema.py +119 -0
- toolbound/core/ratelimit.py +65 -0
- toolbound/core/result.py +100 -0
- toolbound/core/schema.py +201 -0
- toolbound/core/shaping.py +711 -0
- toolbound/core/signing.py +137 -0
- toolbound/core/state.py +102 -0
- toolbound/estimators.py +24 -0
- toolbound/loaders/__init__.py +202 -0
- toolbound/loaders/meta_schema.py +162 -0
- toolbound/py.typed +0 -0
- toolbound/sync.py +76 -0
- toolbound/transport/__init__.py +19 -0
- toolbound/transport/http.py +269 -0
- toolbound/transport/mock.py +89 -0
- toolbound-0.2.0.dist-info/METADATA +547 -0
- toolbound-0.2.0.dist-info/RECORD +39 -0
- toolbound-0.2.0.dist-info/WHEEL +4 -0
- toolbound-0.2.0.dist-info/entry_points.txt +2 -0
- toolbound-0.2.0.dist-info/licenses/LICENSE +202 -0
toolbound/__init__.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Toolbound: contract-first, secure-by-default tools for AI agents."""
|
|
2
|
+
|
|
3
|
+
from toolbound.core.approval import PendingApproval
|
|
4
|
+
from toolbound.core.budget import ByteEstimator
|
|
5
|
+
from toolbound.core.context import ExecutionContext
|
|
6
|
+
from toolbound.core.contracts import (
|
|
7
|
+
ApiKeyAuth,
|
|
8
|
+
BasicAuth,
|
|
9
|
+
BearerAuth,
|
|
10
|
+
Const,
|
|
11
|
+
Effect,
|
|
12
|
+
Field,
|
|
13
|
+
FromContext,
|
|
14
|
+
Limits,
|
|
15
|
+
NetworkPolicy,
|
|
16
|
+
NoAuth,
|
|
17
|
+
Operation,
|
|
18
|
+
Output,
|
|
19
|
+
Pagination,
|
|
20
|
+
PythonTool,
|
|
21
|
+
RateLimit,
|
|
22
|
+
RetryPolicy,
|
|
23
|
+
Service,
|
|
24
|
+
Tool,
|
|
25
|
+
ToolSpec,
|
|
26
|
+
from_ctx,
|
|
27
|
+
)
|
|
28
|
+
from toolbound.core.credentials import EnvCredentials, StaticCredentials
|
|
29
|
+
from toolbound.core.errors import (
|
|
30
|
+
ApprovalInvalid,
|
|
31
|
+
ConfigurationError,
|
|
32
|
+
ErrorCode,
|
|
33
|
+
NetworkBlocked,
|
|
34
|
+
StateStoreError,
|
|
35
|
+
ToolboundError,
|
|
36
|
+
ToolboundWarning,
|
|
37
|
+
ToolFailure,
|
|
38
|
+
)
|
|
39
|
+
from toolbound.core.exposure import ToolDefinition
|
|
40
|
+
from toolbound.core.gateway import Gateway, allow_all, deny_all
|
|
41
|
+
from toolbound.core.observe import Event, LoggingObserver
|
|
42
|
+
from toolbound.core.ports import (
|
|
43
|
+
AuthorizationRequest,
|
|
44
|
+
Decision,
|
|
45
|
+
HttpRequest,
|
|
46
|
+
HttpResponse,
|
|
47
|
+
StateStore,
|
|
48
|
+
TokenEstimator,
|
|
49
|
+
Transport,
|
|
50
|
+
)
|
|
51
|
+
from toolbound.core.result import ResultMeta, ToolError, ToolResult
|
|
52
|
+
from toolbound.core.shaping import Omission
|
|
53
|
+
from toolbound.core.state import MemoryStateStore
|
|
54
|
+
from toolbound.transport.mock import MockResponse, MockTransport
|
|
55
|
+
|
|
56
|
+
__version__ = "0.2.0"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name: str) -> object:
|
|
60
|
+
if name == "SecureTransport":
|
|
61
|
+
from toolbound.transport.http import SecureTransport
|
|
62
|
+
|
|
63
|
+
return SecureTransport
|
|
64
|
+
raise AttributeError(name)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"ApiKeyAuth",
|
|
69
|
+
"ApprovalInvalid",
|
|
70
|
+
"AuthorizationRequest",
|
|
71
|
+
"BasicAuth",
|
|
72
|
+
"BearerAuth",
|
|
73
|
+
"ByteEstimator",
|
|
74
|
+
"ConfigurationError",
|
|
75
|
+
"Const",
|
|
76
|
+
"Decision",
|
|
77
|
+
"Effect",
|
|
78
|
+
"EnvCredentials",
|
|
79
|
+
"ErrorCode",
|
|
80
|
+
"Event",
|
|
81
|
+
"ExecutionContext",
|
|
82
|
+
"Field",
|
|
83
|
+
"FromContext",
|
|
84
|
+
"Gateway",
|
|
85
|
+
"HttpRequest",
|
|
86
|
+
"HttpResponse",
|
|
87
|
+
"Limits",
|
|
88
|
+
"LoggingObserver",
|
|
89
|
+
"MemoryStateStore",
|
|
90
|
+
"MockResponse",
|
|
91
|
+
"MockTransport",
|
|
92
|
+
"NetworkBlocked",
|
|
93
|
+
"NetworkPolicy",
|
|
94
|
+
"NoAuth",
|
|
95
|
+
"Omission",
|
|
96
|
+
"Operation",
|
|
97
|
+
"Output",
|
|
98
|
+
"Pagination",
|
|
99
|
+
"PendingApproval",
|
|
100
|
+
"PythonTool",
|
|
101
|
+
"RateLimit",
|
|
102
|
+
"ResultMeta",
|
|
103
|
+
"RetryPolicy",
|
|
104
|
+
"SecureTransport",
|
|
105
|
+
"Service",
|
|
106
|
+
"StateStore",
|
|
107
|
+
"StateStoreError",
|
|
108
|
+
"StaticCredentials",
|
|
109
|
+
"TokenEstimator",
|
|
110
|
+
"Tool",
|
|
111
|
+
"ToolDefinition",
|
|
112
|
+
"ToolError",
|
|
113
|
+
"ToolFailure",
|
|
114
|
+
"ToolResult",
|
|
115
|
+
"ToolSpec",
|
|
116
|
+
"ToolboundError",
|
|
117
|
+
"ToolboundWarning",
|
|
118
|
+
"Transport",
|
|
119
|
+
"__version__",
|
|
120
|
+
"allow_all",
|
|
121
|
+
"deny_all",
|
|
122
|
+
"from_ctx",
|
|
123
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Optional provider adapters.
|
|
2
|
+
|
|
3
|
+
Each adapter converts Toolbound definitions to a provider's tool format and runs
|
|
4
|
+
the provider's tool calls through the Gateway. None of them import a provider
|
|
5
|
+
SDK, and none contain security logic: everything is enforced by the core.
|
|
6
|
+
|
|
7
|
+
* :mod:`toolbound.adapters.anthropic` - Anthropic Messages API
|
|
8
|
+
* :mod:`toolbound.adapters.openai` - OpenAI Chat Completions and Responses APIs
|
|
9
|
+
"""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shared helpers for provider adapters (no third-party imports)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from toolbound.core.approval import PendingApproval
|
|
12
|
+
from toolbound.core.context import ExecutionContext
|
|
13
|
+
from toolbound.core.errors import ErrorCode
|
|
14
|
+
from toolbound.core.gateway import Gateway
|
|
15
|
+
from toolbound.core.result import ToolError, ToolResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get(obj: Any, key: str, default: Any = None) -> Any:
|
|
19
|
+
"""Read ``key`` from a dict or an SDK object."""
|
|
20
|
+
if isinstance(obj, dict):
|
|
21
|
+
return obj.get(key, default)
|
|
22
|
+
return getattr(obj, key, default)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def optional_id(value: Any) -> str | None:
|
|
26
|
+
"""A provider tool-call id, or None when missing (never the string "None",
|
|
27
|
+
which would make every id-less call look like a duplicate of the first)."""
|
|
28
|
+
return None if value is None or value == "" else str(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class Call:
|
|
33
|
+
id: str | None
|
|
34
|
+
name: str
|
|
35
|
+
arguments: Any
|
|
36
|
+
parse_error: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_json_arguments(raw: Any) -> tuple[Any, str | None]:
|
|
40
|
+
if isinstance(raw, dict):
|
|
41
|
+
return raw, None
|
|
42
|
+
if raw is None or raw == "":
|
|
43
|
+
return {}, None
|
|
44
|
+
if isinstance(raw, str):
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(raw), None
|
|
47
|
+
except ValueError:
|
|
48
|
+
return None, "tool arguments were not valid JSON"
|
|
49
|
+
return None, "tool arguments must be a JSON object"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class ToolCallBatch:
|
|
54
|
+
"""Results of executing one model turn's tool calls.
|
|
55
|
+
|
|
56
|
+
``messages`` (or ``blocks``) are ready to append to the conversation.
|
|
57
|
+
``results`` holds the full :class:`ToolResult` objects for host code, and
|
|
58
|
+
``pending_approvals`` lists calls waiting for a human decision.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
results: list[ToolResult] = field(default_factory=list)
|
|
62
|
+
blocks: list[dict[str, Any]] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def pending_approvals(self) -> list[PendingApproval]:
|
|
66
|
+
return [r.approval for r in self.results if r.approval is not None]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def execute_calls(
|
|
70
|
+
gateway: Gateway, calls: Sequence[Call], ctx: ExecutionContext, *, parallel: bool
|
|
71
|
+
) -> list[ToolResult]:
|
|
72
|
+
async def one(call: Call) -> ToolResult:
|
|
73
|
+
if call.parse_error is not None:
|
|
74
|
+
return ToolResult(
|
|
75
|
+
ok=False,
|
|
76
|
+
tool=call.name,
|
|
77
|
+
error=ToolError(ErrorCode.INVALID_ARGUMENTS, call.parse_error),
|
|
78
|
+
call_id=call.id,
|
|
79
|
+
)
|
|
80
|
+
return await gateway.call(call.name, call.arguments, ctx=ctx, call_id=call.id)
|
|
81
|
+
|
|
82
|
+
if parallel:
|
|
83
|
+
return list(await asyncio.gather(*(one(c) for c in calls)))
|
|
84
|
+
return [await one(c) for c in calls]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Anthropic Messages API adapter. Does not import the ``anthropic`` package.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from toolbound.adapters import anthropic as tb
|
|
5
|
+
|
|
6
|
+
tools = tb.tools(gateway, ctx)
|
|
7
|
+
response = client.messages.create(model=..., tools=tools, messages=messages, max_tokens=...)
|
|
8
|
+
batch = await tb.run_tool_calls(gateway, response, ctx)
|
|
9
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
10
|
+
messages.append(batch.message)
|
|
11
|
+
```
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from toolbound.core.context import ExecutionContext
|
|
20
|
+
from toolbound.core.exposure import Strategy
|
|
21
|
+
from toolbound.core.gateway import Gateway
|
|
22
|
+
|
|
23
|
+
from ._common import Call, ToolCallBatch, execute_calls, get, optional_id, parse_json_arguments
|
|
24
|
+
|
|
25
|
+
__all__ = ["AnthropicToolCallBatch", "run_tool_calls", "tool_calls", "tools"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tools(
|
|
29
|
+
gateway: Gateway,
|
|
30
|
+
ctx: ExecutionContext,
|
|
31
|
+
*,
|
|
32
|
+
tags: Iterable[str] | None = None,
|
|
33
|
+
names: Iterable[str] | None = None,
|
|
34
|
+
strategy: Strategy = "direct",
|
|
35
|
+
) -> list[dict[str, Any]]:
|
|
36
|
+
"""Tool definitions in Anthropic format, filtered for ``ctx``."""
|
|
37
|
+
return [
|
|
38
|
+
{"name": d.name, "description": d.description, "input_schema": d.input_schema}
|
|
39
|
+
for d in gateway.definitions(ctx, tags=tags, names=names, strategy=strategy)
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tool_calls(message: Any) -> list[Call]:
|
|
44
|
+
"""Extract ``tool_use`` blocks from a Message, a content list, or dicts."""
|
|
45
|
+
content = get(message, "content", message)
|
|
46
|
+
calls: list[Call] = []
|
|
47
|
+
for block in content or []:
|
|
48
|
+
if get(block, "type") != "tool_use":
|
|
49
|
+
continue
|
|
50
|
+
arguments, error = parse_json_arguments(get(block, "input"))
|
|
51
|
+
calls.append(Call(optional_id(get(block, "id")), str(get(block, "name")), arguments, error))
|
|
52
|
+
return calls
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AnthropicToolCallBatch(ToolCallBatch):
|
|
56
|
+
@property
|
|
57
|
+
def message(self) -> dict[str, Any]:
|
|
58
|
+
"""A ``user`` message containing every ``tool_result`` block."""
|
|
59
|
+
return {"role": "user", "content": list(self.blocks)}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def run_tool_calls(
|
|
63
|
+
gateway: Gateway, message: Any, ctx: ExecutionContext, *, parallel: bool = False
|
|
64
|
+
) -> AnthropicToolCallBatch:
|
|
65
|
+
"""Execute every ``tool_use`` block and build matching ``tool_result`` blocks.
|
|
66
|
+
|
|
67
|
+
All results belong in a single user message (see :attr:`message`).
|
|
68
|
+
"""
|
|
69
|
+
calls = tool_calls(message)
|
|
70
|
+
results = await execute_calls(gateway, calls, ctx, parallel=parallel)
|
|
71
|
+
batch = AnthropicToolCallBatch(results=results)
|
|
72
|
+
for call, result in zip(calls, results, strict=True):
|
|
73
|
+
block: dict[str, Any] = {
|
|
74
|
+
"type": "tool_result",
|
|
75
|
+
"tool_use_id": call.id,
|
|
76
|
+
"content": result.to_json(),
|
|
77
|
+
}
|
|
78
|
+
if not result.ok:
|
|
79
|
+
block["is_error"] = True
|
|
80
|
+
batch.blocks.append(block)
|
|
81
|
+
return batch
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""OpenAI adapter (Chat Completions and Responses APIs). Does not import ``openai``.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from toolbound.adapters import openai as tb
|
|
5
|
+
|
|
6
|
+
# Chat Completions
|
|
7
|
+
tools = tb.tools(gateway, ctx)
|
|
8
|
+
completion = client.chat.completions.create(model=..., messages=messages, tools=tools)
|
|
9
|
+
message = completion.choices[0].message
|
|
10
|
+
batch = await tb.run_tool_calls(gateway, message, ctx)
|
|
11
|
+
messages.append(message)
|
|
12
|
+
messages.extend(batch.blocks) # role="tool" messages
|
|
13
|
+
|
|
14
|
+
# Responses API
|
|
15
|
+
tools = tb.tools(gateway, ctx, api="responses")
|
|
16
|
+
response = client.responses.create(model=..., input=items, tools=tools)
|
|
17
|
+
batch = await tb.run_tool_calls(gateway, response, ctx)
|
|
18
|
+
items.extend(response.output)
|
|
19
|
+
items.extend(batch.blocks) # function_call_output items
|
|
20
|
+
```
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
from typing import Any, Literal
|
|
27
|
+
|
|
28
|
+
from toolbound.core.context import ExecutionContext
|
|
29
|
+
from toolbound.core.exposure import Strategy
|
|
30
|
+
from toolbound.core.gateway import Gateway
|
|
31
|
+
|
|
32
|
+
from ._common import Call, ToolCallBatch, execute_calls, get, optional_id, parse_json_arguments
|
|
33
|
+
|
|
34
|
+
__all__ = ["run_tool_calls", "tool_calls", "tools"]
|
|
35
|
+
|
|
36
|
+
Api = Literal["chat", "responses"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def tools(
|
|
40
|
+
gateway: Gateway,
|
|
41
|
+
ctx: ExecutionContext,
|
|
42
|
+
*,
|
|
43
|
+
api: Api = "chat",
|
|
44
|
+
tags: Iterable[str] | None = None,
|
|
45
|
+
names: Iterable[str] | None = None,
|
|
46
|
+
strategy: Strategy = "direct",
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
"""Function-tool definitions for Chat Completions (default) or Responses."""
|
|
49
|
+
out: list[dict[str, Any]] = []
|
|
50
|
+
for d in gateway.definitions(ctx, tags=tags, names=names, strategy=strategy):
|
|
51
|
+
if api == "responses":
|
|
52
|
+
out.append(
|
|
53
|
+
{
|
|
54
|
+
"type": "function",
|
|
55
|
+
"name": d.name,
|
|
56
|
+
"description": d.description,
|
|
57
|
+
"parameters": d.input_schema,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
out.append(
|
|
62
|
+
{
|
|
63
|
+
"type": "function",
|
|
64
|
+
"function": {
|
|
65
|
+
"name": d.name,
|
|
66
|
+
"description": d.description,
|
|
67
|
+
"parameters": d.input_schema,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def tool_calls(obj: Any) -> tuple[list[Call], Api]:
|
|
75
|
+
"""Extract calls from a chat message/completion or a Responses API response."""
|
|
76
|
+
output = get(obj, "output")
|
|
77
|
+
if isinstance(output, list):
|
|
78
|
+
calls = []
|
|
79
|
+
for item in output:
|
|
80
|
+
if get(item, "type") != "function_call":
|
|
81
|
+
continue
|
|
82
|
+
arguments, error = parse_json_arguments(get(item, "arguments"))
|
|
83
|
+
calls.append(
|
|
84
|
+
Call(optional_id(get(item, "call_id")), str(get(item, "name")), arguments, error)
|
|
85
|
+
)
|
|
86
|
+
return calls, "responses"
|
|
87
|
+
choices = get(obj, "choices")
|
|
88
|
+
message = get(choices[0], "message") if choices else obj
|
|
89
|
+
calls = []
|
|
90
|
+
for item in get(message, "tool_calls") or []:
|
|
91
|
+
function = get(item, "function")
|
|
92
|
+
arguments, error = parse_json_arguments(get(function, "arguments"))
|
|
93
|
+
calls.append(
|
|
94
|
+
Call(optional_id(get(item, "id")), str(get(function, "name")), arguments, error)
|
|
95
|
+
)
|
|
96
|
+
return calls, "chat"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def run_tool_calls(
|
|
100
|
+
gateway: Gateway, obj: Any, ctx: ExecutionContext, *, parallel: bool = False
|
|
101
|
+
) -> ToolCallBatch:
|
|
102
|
+
"""Execute every function call and build the matching result items."""
|
|
103
|
+
calls, api = tool_calls(obj)
|
|
104
|
+
results = await execute_calls(gateway, calls, ctx, parallel=parallel)
|
|
105
|
+
batch = ToolCallBatch(results=results)
|
|
106
|
+
for call, result in zip(calls, results, strict=True):
|
|
107
|
+
if api == "responses":
|
|
108
|
+
batch.blocks.append(
|
|
109
|
+
{"type": "function_call_output", "call_id": call.id, "output": result.to_json()}
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
batch.blocks.append(
|
|
113
|
+
{"role": "tool", "tool_call_id": call.id, "content": result.to_json()}
|
|
114
|
+
)
|
|
115
|
+
return batch
|
toolbound/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""``toolbound`` command line: validate contracts and preview tool definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from toolbound.core.budget import ByteEstimator
|
|
11
|
+
from toolbound.core.context import ExecutionContext
|
|
12
|
+
from toolbound.core.errors import ConfigurationError
|
|
13
|
+
from toolbound.core.gateway import Gateway, allow_all
|
|
14
|
+
from toolbound.loaders import CONTRACT_SCHEMA
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _gateway(path: str) -> Gateway:
|
|
18
|
+
return Gateway(authorize=allow_all).load(path)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
22
|
+
parser = argparse.ArgumentParser(prog="toolbound", description=__doc__)
|
|
23
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
24
|
+
validate = sub.add_parser("validate", help="validate contract files")
|
|
25
|
+
validate.add_argument("path")
|
|
26
|
+
show = sub.add_parser("tools", help="print model-visible tool definitions")
|
|
27
|
+
show.add_argument("path")
|
|
28
|
+
show.add_argument("--format", choices=["anthropic", "openai", "neutral"], default="neutral")
|
|
29
|
+
show.add_argument("--strategy", choices=["direct", "grouped"], default="direct")
|
|
30
|
+
show.add_argument(
|
|
31
|
+
"--scope",
|
|
32
|
+
action="append",
|
|
33
|
+
default=[],
|
|
34
|
+
help="treat the viewer as having this scope (repeatable)",
|
|
35
|
+
)
|
|
36
|
+
sub.add_parser("schema", help="print the JSON Schema of contract files")
|
|
37
|
+
args = parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
if args.command == "schema":
|
|
40
|
+
print(json.dumps(CONTRACT_SCHEMA, indent=2))
|
|
41
|
+
return 0
|
|
42
|
+
try:
|
|
43
|
+
gateway = _gateway(args.path)
|
|
44
|
+
except (ConfigurationError, OSError, ValueError) as exc:
|
|
45
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
46
|
+
return 1
|
|
47
|
+
if args.command == "validate":
|
|
48
|
+
print(f"ok: {len(gateway.tool_names)} tool(s): {', '.join(gateway.tool_names)}")
|
|
49
|
+
return 0
|
|
50
|
+
ctx = ExecutionContext(actor="cli", scopes=frozenset(args.scope))
|
|
51
|
+
if args.format == "anthropic":
|
|
52
|
+
from toolbound.adapters import anthropic
|
|
53
|
+
|
|
54
|
+
payload: object = anthropic.tools(gateway, ctx, strategy=args.strategy)
|
|
55
|
+
elif args.format == "openai":
|
|
56
|
+
from toolbound.adapters import openai
|
|
57
|
+
|
|
58
|
+
payload = openai.tools(gateway, ctx, strategy=args.strategy)
|
|
59
|
+
else:
|
|
60
|
+
payload = [d.to_dict() for d in gateway.definitions(ctx, strategy=args.strategy)]
|
|
61
|
+
text = json.dumps(payload, indent=2, ensure_ascii=False)
|
|
62
|
+
print(text)
|
|
63
|
+
compact = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
|
|
64
|
+
print(f"# ~{ByteEstimator().count(compact)} tokens (byte estimate)", file=sys.stderr)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__": # pragma: no cover
|
|
69
|
+
raise SystemExit(main())
|