hidden-moves-openai 0.1.0__tar.gz
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.
- hidden_moves_openai-0.1.0/LICENSE +21 -0
- hidden_moves_openai-0.1.0/PKG-INFO +121 -0
- hidden_moves_openai-0.1.0/README.md +98 -0
- hidden_moves_openai-0.1.0/pyproject.toml +39 -0
- hidden_moves_openai-0.1.0/pyproject.toml.orig +30 -0
- hidden_moves_openai-0.1.0/src/hidden_moves_openai/__init__.py +5 -0
- hidden_moves_openai-0.1.0/src/hidden_moves_openai/adapter.py +141 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ella Inng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hidden-moves-openai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Export selected Python capabilities as OpenAI Responses function tools.
|
|
5
|
+
Keywords: openai,responses,function-calling,tools
|
|
6
|
+
Author: Ella Inng
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Dist: hidden-moves>=0.1,<0.2
|
|
16
|
+
Maintainer: Outside Labs
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/outside-labs/hidden_moves
|
|
19
|
+
Project-URL: Source, https://github.com/outside-labs/hidden_moves
|
|
20
|
+
Project-URL: Issues, https://github.com/outside-labs/hidden_moves/issues
|
|
21
|
+
Project-URL: Documentation, https://github.com/outside-labs/hidden_moves/blob/main/packages/hidden-moves-openai/README.md
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Function-tool adapter
|
|
25
|
+
|
|
26
|
+
This separate distribution exports a selected `CapabilityCatalog` as OpenAI
|
|
27
|
+
Responses function tools and dispatches returned calls through that catalog.
|
|
28
|
+
It uses the standard library and the core package; an API SDK is optional in the
|
|
29
|
+
application that consumes it.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
python -m pip install hidden-moves-openai==0.1.0
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The experimental 0.1.x adapter depends on `hidden-moves>=0.1,<0.2` and Python
|
|
36
|
+
3.11 or newer. Published GitHub releases use `v0.1.0-openai` for this package in
|
|
37
|
+
the shared repository. `.github/workflows/release.yaml` checks installed-wheel
|
|
38
|
+
offline export and dispatch, then publishes in environment `pypi-openai`.
|
|
39
|
+
|
|
40
|
+
## Local setup
|
|
41
|
+
|
|
42
|
+
From the repository root, install both distributions in your chosen environment:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv pip install --python .venv/bin/python -e . -e ./examples/text-plugin -e ./packages/hidden-moves-openai
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Export and dispatch work offline:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
import json
|
|
53
|
+
|
|
54
|
+
from hidden_moves import Moves
|
|
55
|
+
from hidden_moves.adapters import CapabilityCatalog
|
|
56
|
+
from hidden_moves_example_text import repeat_text
|
|
57
|
+
from hidden_moves_openai import FunctionToolAdapter
|
|
58
|
+
|
|
59
|
+
moves = Moves()
|
|
60
|
+
moves.learn(repeat_text, name="repeat", namespace="example.text")
|
|
61
|
+
adapter = FunctionToolAdapter(CapabilityCatalog(moves, ["example.text.repeat"]))
|
|
62
|
+
print(json.dumps(adapter.tools(), indent=2))
|
|
63
|
+
|
|
64
|
+
output = asyncio.run(adapter.call_output(
|
|
65
|
+
"local-call-1", "example__text__repeat", '{"value": "Hello World"}',
|
|
66
|
+
))
|
|
67
|
+
assert json.loads(output["output"]) == "Hello World Hello World"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In an existing asynchronous Responses application, pass `adapter.tools()` as
|
|
71
|
+
`tools`. For each returned `function_call` item, use
|
|
72
|
+
`await adapter.call_output(item.call_id, item.name, item.arguments)` to construct
|
|
73
|
+
its result item. The application keeps the response history, sends results to
|
|
74
|
+
the API, and decides whether to request another response. The adapter owns no
|
|
75
|
+
API client, credentials, model choice, retries, approvals, or event loop.
|
|
76
|
+
|
|
77
|
+
## Names and schemas
|
|
78
|
+
|
|
79
|
+
Dots become double underscores: `example.text.repeat` exports as `example__text__repeat`.
|
|
80
|
+
Names use 1-64 ASCII letters, digits, underscores, or hyphens. Invalid names and
|
|
81
|
+
collisions fail at construction; use `tool_names={"qualified.name": "alias"}`
|
|
82
|
+
for an explicit mapping. The original catalog retains its qualified names.
|
|
83
|
+
|
|
84
|
+
Tool dictionaries use the flat Responses format, with explicit `strict=False`
|
|
85
|
+
by default. This preserves omitted Python defaults. Schema dialect declarations,
|
|
86
|
+
defaults, and examples are omitted from the exported parameters. Property names
|
|
87
|
+
and neutral definitions remain unchanged. Behavioral hints and arbitrary metadata
|
|
88
|
+
stay in the catalog rather than becoming unsupported API fields.
|
|
89
|
+
|
|
90
|
+
`strict=True` accepts a conservative structural subset: each object is closed
|
|
91
|
+
and requires every property; arrays have typed items; unions use `anyOf` or a
|
|
92
|
+
nullable type pair. Scalar enums gain an inferred type when possible. Optional
|
|
93
|
+
properties, open mappings, untyped values, and mixed untyped enums fail exposure.
|
|
94
|
+
The adapter does not replace omitted arguments with `null` or invent required
|
|
95
|
+
defaults. Model-specific schema size limits remain the application's concern.
|
|
96
|
+
|
|
97
|
+
These choices follow the official [function calling guide](https://developers.openai.com/api/docs/guides/function-calling)
|
|
98
|
+
and [Structured Outputs requirements](https://developers.openai.com/api/docs/guides/structured-outputs),
|
|
99
|
+
checked October 3, 2026. Local tests validate export and dispatch. Live API
|
|
100
|
+
acceptance and a model conversation have not been tested.
|
|
101
|
+
|
|
102
|
+
## Invocation and errors
|
|
103
|
+
|
|
104
|
+
`call(name, arguments)` accepts a JSON string or mapping and returns JSON text.
|
|
105
|
+
`call_output` adds the supplied call ID in a `function_call_output` item.
|
|
106
|
+
Only selected aliases can dispatch, and validation happens before invocation.
|
|
107
|
+
Async functions and returned awaitables run in the caller's existing loop;
|
|
108
|
+
synchronous functions run inline. Results use the catalog's dataclass, enum,
|
|
109
|
+
TypedDict, and finite JSON conversions and output validation.
|
|
110
|
+
|
|
111
|
+
Invalid arguments raise `CapabilityArgumentError`; invalid results raise
|
|
112
|
+
`CapabilityResultError`; unselected names raise `UnknownMoveError`. Capability
|
|
113
|
+
exceptions and cancellation propagate to the application. The application must
|
|
114
|
+
choose its error reporting and invocation policy before returning failures to
|
|
115
|
+
a remote service.
|
|
116
|
+
|
|
117
|
+
Run the adapter tests in an environment with both local packages installed:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
python -m unittest discover -s packages/hidden-moves-openai/tests -v
|
|
121
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Function-tool adapter
|
|
2
|
+
|
|
3
|
+
This separate distribution exports a selected `CapabilityCatalog` as OpenAI
|
|
4
|
+
Responses function tools and dispatches returned calls through that catalog.
|
|
5
|
+
It uses the standard library and the core package; an API SDK is optional in the
|
|
6
|
+
application that consumes it.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
python -m pip install hidden-moves-openai==0.1.0
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The experimental 0.1.x adapter depends on `hidden-moves>=0.1,<0.2` and Python
|
|
13
|
+
3.11 or newer. Published GitHub releases use `v0.1.0-openai` for this package in
|
|
14
|
+
the shared repository. `.github/workflows/release.yaml` checks installed-wheel
|
|
15
|
+
offline export and dispatch, then publishes in environment `pypi-openai`.
|
|
16
|
+
|
|
17
|
+
## Local setup
|
|
18
|
+
|
|
19
|
+
From the repository root, install both distributions in your chosen environment:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
uv pip install --python .venv/bin/python -e . -e ./examples/text-plugin -e ./packages/hidden-moves-openai
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Export and dispatch work offline:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import asyncio
|
|
29
|
+
import json
|
|
30
|
+
|
|
31
|
+
from hidden_moves import Moves
|
|
32
|
+
from hidden_moves.adapters import CapabilityCatalog
|
|
33
|
+
from hidden_moves_example_text import repeat_text
|
|
34
|
+
from hidden_moves_openai import FunctionToolAdapter
|
|
35
|
+
|
|
36
|
+
moves = Moves()
|
|
37
|
+
moves.learn(repeat_text, name="repeat", namespace="example.text")
|
|
38
|
+
adapter = FunctionToolAdapter(CapabilityCatalog(moves, ["example.text.repeat"]))
|
|
39
|
+
print(json.dumps(adapter.tools(), indent=2))
|
|
40
|
+
|
|
41
|
+
output = asyncio.run(adapter.call_output(
|
|
42
|
+
"local-call-1", "example__text__repeat", '{"value": "Hello World"}',
|
|
43
|
+
))
|
|
44
|
+
assert json.loads(output["output"]) == "Hello World Hello World"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In an existing asynchronous Responses application, pass `adapter.tools()` as
|
|
48
|
+
`tools`. For each returned `function_call` item, use
|
|
49
|
+
`await adapter.call_output(item.call_id, item.name, item.arguments)` to construct
|
|
50
|
+
its result item. The application keeps the response history, sends results to
|
|
51
|
+
the API, and decides whether to request another response. The adapter owns no
|
|
52
|
+
API client, credentials, model choice, retries, approvals, or event loop.
|
|
53
|
+
|
|
54
|
+
## Names and schemas
|
|
55
|
+
|
|
56
|
+
Dots become double underscores: `example.text.repeat` exports as `example__text__repeat`.
|
|
57
|
+
Names use 1-64 ASCII letters, digits, underscores, or hyphens. Invalid names and
|
|
58
|
+
collisions fail at construction; use `tool_names={"qualified.name": "alias"}`
|
|
59
|
+
for an explicit mapping. The original catalog retains its qualified names.
|
|
60
|
+
|
|
61
|
+
Tool dictionaries use the flat Responses format, with explicit `strict=False`
|
|
62
|
+
by default. This preserves omitted Python defaults. Schema dialect declarations,
|
|
63
|
+
defaults, and examples are omitted from the exported parameters. Property names
|
|
64
|
+
and neutral definitions remain unchanged. Behavioral hints and arbitrary metadata
|
|
65
|
+
stay in the catalog rather than becoming unsupported API fields.
|
|
66
|
+
|
|
67
|
+
`strict=True` accepts a conservative structural subset: each object is closed
|
|
68
|
+
and requires every property; arrays have typed items; unions use `anyOf` or a
|
|
69
|
+
nullable type pair. Scalar enums gain an inferred type when possible. Optional
|
|
70
|
+
properties, open mappings, untyped values, and mixed untyped enums fail exposure.
|
|
71
|
+
The adapter does not replace omitted arguments with `null` or invent required
|
|
72
|
+
defaults. Model-specific schema size limits remain the application's concern.
|
|
73
|
+
|
|
74
|
+
These choices follow the official [function calling guide](https://developers.openai.com/api/docs/guides/function-calling)
|
|
75
|
+
and [Structured Outputs requirements](https://developers.openai.com/api/docs/guides/structured-outputs),
|
|
76
|
+
checked October 3, 2026. Local tests validate export and dispatch. Live API
|
|
77
|
+
acceptance and a model conversation have not been tested.
|
|
78
|
+
|
|
79
|
+
## Invocation and errors
|
|
80
|
+
|
|
81
|
+
`call(name, arguments)` accepts a JSON string or mapping and returns JSON text.
|
|
82
|
+
`call_output` adds the supplied call ID in a `function_call_output` item.
|
|
83
|
+
Only selected aliases can dispatch, and validation happens before invocation.
|
|
84
|
+
Async functions and returned awaitables run in the caller's existing loop;
|
|
85
|
+
synchronous functions run inline. Results use the catalog's dataclass, enum,
|
|
86
|
+
TypedDict, and finite JSON conversions and output validation.
|
|
87
|
+
|
|
88
|
+
Invalid arguments raise `CapabilityArgumentError`; invalid results raise
|
|
89
|
+
`CapabilityResultError`; unselected names raise `UnknownMoveError`. Capability
|
|
90
|
+
exceptions and cancellation propagate to the application. The application must
|
|
91
|
+
choose its error reporting and invocation policy before returning failures to
|
|
92
|
+
a remote service.
|
|
93
|
+
|
|
94
|
+
Run the adapter tests in an environment with both local packages installed:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
python -m unittest discover -s packages/hidden-moves-openai/tests -v
|
|
98
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hidden-moves-openai"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Export selected Python capabilities as OpenAI Responses function tools."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"openai",
|
|
11
|
+
"responses",
|
|
12
|
+
"function-calling",
|
|
13
|
+
"tools",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["hidden-moves>=0.1,<0.2"]
|
|
24
|
+
|
|
25
|
+
[[project.authors]]
|
|
26
|
+
name = "Ella Inng"
|
|
27
|
+
|
|
28
|
+
[[project.maintainers]]
|
|
29
|
+
name = "Outside Labs"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/outside-labs/hidden_moves"
|
|
33
|
+
Source = "https://github.com/outside-labs/hidden_moves"
|
|
34
|
+
Issues = "https://github.com/outside-labs/hidden_moves/issues"
|
|
35
|
+
Documentation = "https://github.com/outside-labs/hidden_moves/blob/main/packages/hidden-moves-openai/README.md"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.12.1,<0.13.0"]
|
|
39
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hidden-moves-openai"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Export selected Python capabilities as OpenAI Responses function tools."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Ella Inng" }]
|
|
10
|
+
maintainers = [{ name = "Outside Labs" }]
|
|
11
|
+
keywords = ["openai", "responses", "function-calling", "tools"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["hidden-moves>=0.1,<0.2"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/outside-labs/hidden_moves"
|
|
24
|
+
Source = "https://github.com/outside-labs/hidden_moves"
|
|
25
|
+
Issues = "https://github.com/outside-labs/hidden_moves/issues"
|
|
26
|
+
Documentation = "https://github.com/outside-labs/hidden_moves/blob/main/packages/hidden-moves-openai/README.md"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.12.1,<0.13.0"]
|
|
30
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Responses function tools consume the same selected catalog as other interfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from hidden_moves import UnknownMoveError
|
|
13
|
+
from hidden_moves.adapters import CapabilityArgumentError, CapabilityCatalog, CapabilityExposureError
|
|
14
|
+
|
|
15
|
+
_TOOL_NAME = re.compile(r"[A-Za-z0-9_-]{1,64}\Z")
|
|
16
|
+
_OMITTED_KEYWORDS = frozenset({"$schema", "default", "examples"})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parameters(schema: dict[str, Any]) -> dict[str, Any]:
|
|
20
|
+
"""Drop declaration-only keywords without changing property names or enum values."""
|
|
21
|
+
result = {key: value for key, value in schema.items() if key not in _OMITTED_KEYWORDS}
|
|
22
|
+
if "properties" in result:
|
|
23
|
+
result["properties"] = {name: _parameters(child) for name, child in result["properties"].items()}
|
|
24
|
+
if "anyOf" in result:
|
|
25
|
+
result["anyOf"] = [_parameters(child) for child in result["anyOf"]]
|
|
26
|
+
for key in ("items", "additionalProperties"):
|
|
27
|
+
if isinstance(result.get(key), dict):
|
|
28
|
+
result[key] = _parameters(result[key])
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _enum_type(values: list[Any]) -> str | None:
|
|
33
|
+
kinds = {
|
|
34
|
+
"null" if value is None else "boolean" if type(value) is bool else
|
|
35
|
+
"integer" if type(value) is int else "number" if type(value) is float else
|
|
36
|
+
"string" if isinstance(value, str) else None
|
|
37
|
+
for value in values
|
|
38
|
+
}
|
|
39
|
+
if kinds <= {"integer", "number"}:
|
|
40
|
+
return "number" if "number" in kinds else "integer"
|
|
41
|
+
return next(iter(kinds)) if len(kinds) == 1 else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_strict(schema: dict[str, Any], path: str = "$parameters") -> None:
|
|
45
|
+
"""Accept a conservative subset without rewriting Python omission semantics."""
|
|
46
|
+
def unsupported(reason: str) -> None:
|
|
47
|
+
raise CapabilityExposureError(f"{path}: {reason}; use strict=False or a compatible callable/schema.")
|
|
48
|
+
|
|
49
|
+
if "anyOf" in schema:
|
|
50
|
+
if any(key in schema for key in ("type", "enum", "properties", "items")):
|
|
51
|
+
unsupported("strict unions must use separate anyOf branches")
|
|
52
|
+
for index, branch in enumerate(schema["anyOf"]):
|
|
53
|
+
_check_strict(branch, f"{path}.anyOf[{index}]")
|
|
54
|
+
return
|
|
55
|
+
if "type" not in schema and "enum" in schema:
|
|
56
|
+
kind = _enum_type(schema["enum"])
|
|
57
|
+
if kind is not None:
|
|
58
|
+
schema["type"] = kind
|
|
59
|
+
types = schema.get("type")
|
|
60
|
+
if types is None:
|
|
61
|
+
unsupported("strict schemas require typed values")
|
|
62
|
+
if isinstance(types, list):
|
|
63
|
+
if len(types) != 2 or "null" not in types or len(set(types)) != 2:
|
|
64
|
+
unsupported("strict type lists must contain one type and null")
|
|
65
|
+
else:
|
|
66
|
+
types = [types]
|
|
67
|
+
if "object" in types:
|
|
68
|
+
if schema.get("additionalProperties") is not False:
|
|
69
|
+
unsupported("strict objects must reject additional properties")
|
|
70
|
+
properties = schema.get("properties", {})
|
|
71
|
+
if set(schema.get("required", ())) != set(properties):
|
|
72
|
+
unsupported("strict objects must require every property")
|
|
73
|
+
for name, child in properties.items():
|
|
74
|
+
_check_strict(child, f"{path}.{name}")
|
|
75
|
+
if "array" in types:
|
|
76
|
+
if "items" not in schema:
|
|
77
|
+
unsupported("strict arrays require an item schema")
|
|
78
|
+
_check_strict(schema["items"], f"{path}.items")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FunctionToolAdapter:
|
|
82
|
+
"""Export Responses tool dictionaries and execute calls in the caller's loop."""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
catalog: CapabilityCatalog,
|
|
87
|
+
*,
|
|
88
|
+
tool_names: Mapping[str, str] | None = None,
|
|
89
|
+
strict: bool = False,
|
|
90
|
+
) -> None:
|
|
91
|
+
if type(strict) is not bool:
|
|
92
|
+
raise TypeError("strict must be a boolean.")
|
|
93
|
+
self.catalog = catalog
|
|
94
|
+
aliases = dict(tool_names or {})
|
|
95
|
+
selected = {definition.name for definition in catalog.definitions()}
|
|
96
|
+
if set(aliases) - selected:
|
|
97
|
+
raise ValueError("Tool aliases must reference selected capabilities.")
|
|
98
|
+
self._names: dict[str, str] = {}
|
|
99
|
+
tools = []
|
|
100
|
+
for definition in catalog.definitions():
|
|
101
|
+
name = aliases.get(definition.name, definition.name.replace(".", "__"))
|
|
102
|
+
if not isinstance(name, str) or not _TOOL_NAME.fullmatch(name):
|
|
103
|
+
raise ValueError("Function tool names require 1-64 ASCII letters, digits, underscores, or hyphens; supply an alias.")
|
|
104
|
+
if name in self._names:
|
|
105
|
+
raise ValueError(f"Function tool name collision: {name!r}.")
|
|
106
|
+
parameters = _parameters(definition.to_dict()["input_schema"])
|
|
107
|
+
if strict:
|
|
108
|
+
_check_strict(parameters)
|
|
109
|
+
tool = {"type": "function", "name": name, "parameters": parameters, "strict": strict}
|
|
110
|
+
if definition.description is not None:
|
|
111
|
+
tool["description"] = definition.description
|
|
112
|
+
tools.append(tool)
|
|
113
|
+
self._names[name] = definition.name
|
|
114
|
+
self._tools = tuple(tools)
|
|
115
|
+
|
|
116
|
+
def tools(self) -> list[dict[str, Any]]:
|
|
117
|
+
"""Return fresh flat Responses definitions, ready for an application's request."""
|
|
118
|
+
return deepcopy(list(self._tools))
|
|
119
|
+
|
|
120
|
+
async def call(self, name: str, arguments: str | Mapping[str, Any]) -> str:
|
|
121
|
+
"""Validate selected arguments, await when needed, and return finite JSON text."""
|
|
122
|
+
try:
|
|
123
|
+
qualified_name = self._names[name]
|
|
124
|
+
except KeyError as error:
|
|
125
|
+
raise UnknownMoveError(f"Function tool {name!r} is not selected in this adapter.") from error
|
|
126
|
+
if isinstance(arguments, str):
|
|
127
|
+
try:
|
|
128
|
+
arguments = json.loads(arguments)
|
|
129
|
+
except (ValueError, RecursionError) as error:
|
|
130
|
+
raise CapabilityArgumentError("Function arguments must be valid JSON.") from error
|
|
131
|
+
result = self.catalog.invoke(qualified_name, arguments)
|
|
132
|
+
if inspect.isawaitable(result):
|
|
133
|
+
result = await result
|
|
134
|
+
value = self.catalog.serialize_result(qualified_name, result)
|
|
135
|
+
return json.dumps(value, ensure_ascii=False, allow_nan=False)
|
|
136
|
+
|
|
137
|
+
async def call_output(self, call_id: str, name: str, arguments: str | Mapping[str, Any]) -> dict[str, str]:
|
|
138
|
+
"""Build the application's function_call_output item without contacting an API."""
|
|
139
|
+
if not isinstance(call_id, str) or not call_id:
|
|
140
|
+
raise ValueError("call_id must be a nonempty string.")
|
|
141
|
+
return {"type": "function_call_output", "call_id": call_id, "output": await self.call(name, arguments)}
|