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.
@@ -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,5 @@
1
+ """An optional function-tool format and dispatcher; no API client is required."""
2
+
3
+ from .adapter import FunctionToolAdapter
4
+
5
+ __all__ = ["FunctionToolAdapter"]
@@ -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)}