opentool-python 0.1.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.
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .function_model import FunctionModel
7
+ from .info import Info
8
+ from .schema import Schema, SchemaReferenceResolver, SchemaResolver
9
+ from .server import ServerConfig
10
+
11
+
12
+ @dataclass
13
+ class OpenTool:
14
+ opentool: str
15
+ info: Info
16
+ functions: list[FunctionModel]
17
+ server: ServerConfig | None = None
18
+ schemas: dict[str, Schema] | None = None
19
+
20
+ @classmethod
21
+ def from_json(
22
+ cls,
23
+ data: dict[str, Any],
24
+ *,
25
+ schema_reference_resolver: SchemaReferenceResolver | None = None,
26
+ ) -> "OpenTool":
27
+ raw_schemas = data.get("schemas")
28
+ if raw_schemas is not None and not isinstance(raw_schemas, dict):
29
+ raise ValueError("schemas: expected an object")
30
+ schema_resolver = SchemaResolver(raw_schemas, schema_reference_resolver)
31
+ schemas = schema_resolver.resolve_all() if raw_schemas is not None else None
32
+ functions = data["functions"]
33
+ if not isinstance(functions, list) or any(not isinstance(item, dict) for item in functions):
34
+ raise ValueError("functions: expected an array of objects")
35
+
36
+ return cls(
37
+ opentool=str(data["opentool"]),
38
+ info=Info.from_json(data["info"]),
39
+ server=ServerConfig.from_json(data["server"]) if isinstance(data.get("server"), dict) else None,
40
+ functions=[
41
+ FunctionModel.from_json(item, schema_resolver=schema_resolver)
42
+ for item in functions
43
+ ],
44
+ schemas=schemas,
45
+ )
46
+
47
+ def to_json(self) -> dict[str, Any]:
48
+ payload: dict[str, Any] = {
49
+ "opentool": self.opentool,
50
+ "info": self.info.to_json(),
51
+ "functions": [function.to_json() for function in self.functions],
52
+ }
53
+ if self.server is not None:
54
+ payload["server"] = self.server.to_json()
55
+ if self.schemas is not None:
56
+ payload["schemas"] = {
57
+ name: schema.to_json() for name, schema in self.schemas.items()
58
+ }
59
+ return payload
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .schema import Schema, SchemaResolver
7
+
8
+
9
+ @dataclass
10
+ class Parameter:
11
+ name: str
12
+ schema: Schema
13
+ required: bool = False
14
+ description: str | None = None
15
+
16
+ @classmethod
17
+ def from_json(cls, data: dict[str, Any], *, schema_resolver: SchemaResolver | None = None) -> "Parameter":
18
+ required = data.get("required")
19
+ if required is not None and not isinstance(required, bool):
20
+ raise ValueError("required: expected a boolean")
21
+ return cls(
22
+ name=str(data["name"]),
23
+ schema=Schema.from_json(data["schema"], resolver=schema_resolver),
24
+ required=required if required is not None else False,
25
+ description=data.get("description"),
26
+ )
27
+
28
+ def to_json(self) -> dict[str, Any]:
29
+ payload: dict[str, Any] = {
30
+ "name": self.name,
31
+ "schema": self.schema.to_json(),
32
+ "required": self.required,
33
+ }
34
+ if self.description is not None:
35
+ payload["description"] = self.description
36
+ return payload
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .schema import Schema, SchemaResolver
7
+
8
+
9
+ @dataclass
10
+ class Return:
11
+ name: str
12
+ schema: Schema
13
+ description: str | None = None
14
+
15
+ @classmethod
16
+ def from_json(cls, data: dict[str, Any], *, schema_resolver: SchemaResolver | None = None) -> "Return":
17
+ return cls(
18
+ name=str(data["name"]),
19
+ schema=Schema.from_json(data["schema"], resolver=schema_resolver),
20
+ description=data.get("description"),
21
+ )
22
+
23
+ def to_json(self) -> dict[str, Any]:
24
+ payload: dict[str, Any] = {"name": self.name, "schema": self.schema.to_json()}
25
+ if self.description is not None:
26
+ payload["description"] = self.description
27
+ return payload
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+ from urllib.parse import quote, unquote
8
+
9
+ from jsonschema import Draft202012Validator, FormatChecker
10
+ from jsonschema.validators import validator_for
11
+ from referencing import Registry, Resource
12
+ from referencing.exceptions import NoSuchResource
13
+ from referencing.jsonschema import DRAFT202012
14
+
15
+
16
+ SchemaValue = dict[str, Any] | bool
17
+ SchemaReferenceResolver = Callable[[str], SchemaValue | None]
18
+ _LOCAL_PREFIX = "https://opentool.invalid/schemas/"
19
+
20
+
21
+ class SchemaType:
22
+ BOOLEAN = "boolean"
23
+ INTEGER = "integer"
24
+ NUMBER = "number"
25
+ STRING = "string"
26
+ ARRAY = "array"
27
+ OBJECT = "object"
28
+ NULL = "null"
29
+
30
+
31
+ @dataclass
32
+ class SchemaValidationResult:
33
+ valid: bool
34
+ errors: list[str]
35
+ warnings: list[str] = field(default_factory=list)
36
+
37
+ @property
38
+ def is_valid(self) -> bool:
39
+ return self.valid
40
+
41
+
42
+ class SchemaResolver:
43
+ def __init__(
44
+ self,
45
+ definitions: dict[str, SchemaValue] | None = None,
46
+ reference_resolver: SchemaReferenceResolver | None = None,
47
+ ) -> None:
48
+ self._definitions = deepcopy(definitions) if definitions is not None else {}
49
+
50
+ def retrieve(uri: str) -> Resource:
51
+ value = reference_resolver(uri) if reference_resolver is not None else None
52
+ if value is None:
53
+ raise NoSuchResource(ref=uri)
54
+ return self._resource(value)
55
+
56
+ self.registry = Registry(retrieve=retrieve).with_resources(
57
+ (_LOCAL_PREFIX + quote(name, safe=""), self._resource(value))
58
+ for name, value in self._definitions.items()
59
+ )
60
+
61
+ def resolve_all(self) -> dict[str, Schema]:
62
+ return {name: Schema.from_json(value, resolver=self) for name, value in self._definitions.items()}
63
+
64
+ def _resource(self, value: SchemaValue) -> Resource:
65
+ if not isinstance(value, (dict, bool)):
66
+ raise ValueError("Expected a JSON Schema object or boolean")
67
+ return Resource.from_contents(self.normalize(value), default_specification=DRAFT202012)
68
+
69
+ def keyword_view(self, value: dict[str, Any]) -> dict[str, Any]:
70
+ chain = [value]
71
+ seen = set()
72
+ while isinstance(ref := value.get("$ref"), str) and ref.startswith("#/schemas/"):
73
+ token = ref[len("#/schemas/"):]
74
+ if "/" in token or ref in seen:
75
+ break
76
+ seen.add(ref)
77
+ name = unquote(token).replace("~1", "/").replace("~0", "~")
78
+ target = self._definitions.get(name)
79
+ if not isinstance(target, dict):
80
+ break
81
+ chain.append(target)
82
+ value = target
83
+ view: dict[str, Any] = {}
84
+ for node in reversed(chain):
85
+ view.update(node)
86
+ return view
87
+
88
+ def normalize(self, value: SchemaValue) -> SchemaValue:
89
+ if not isinstance(value, dict):
90
+ return value
91
+ normalized = dict(value)
92
+ # Only visit schema positions, leaving literal values in enum, const and annotations intact.
93
+ for key in ("$ref", "$dynamicRef", "$recursiveRef"):
94
+ ref = value.get(key)
95
+ if isinstance(ref, str) and ref.startswith("#/schemas/"):
96
+ token, separator, rest = ref[len("#/schemas/"):].partition("/")
97
+ name = unquote(token).replace("~1", "/").replace("~0", "~")
98
+ if name not in self._definitions:
99
+ raise ValueError(f"Unknown OpenTool schema reference: {ref}")
100
+ normalized[key] = _LOCAL_PREFIX + quote(name, safe="") + (f"#/{rest}" if separator else "")
101
+ for key in ("$defs", "definitions", "properties", "patternProperties", "dependentSchemas", "dependencies"):
102
+ if isinstance(value.get(key), dict):
103
+ normalized[key] = {name: self.normalize(node) for name, node in value[key].items()}
104
+ for key in ("allOf", "anyOf", "oneOf", "prefixItems", "items"):
105
+ if isinstance(value.get(key), list):
106
+ normalized[key] = [self.normalize(node) for node in value[key]]
107
+ for key in (
108
+ "additionalProperties", "unevaluatedProperties", "items", "additionalItems",
109
+ "unevaluatedItems", "contains", "propertyNames", "not", "if", "then", "else", "contentSchema",
110
+ ):
111
+ if isinstance(value.get(key), (dict, bool)):
112
+ normalized[key] = self.normalize(value[key])
113
+ return normalized
114
+
115
+
116
+ class SchemasSingleton:
117
+ """Legacy registry for standalone Schema.from_json callers."""
118
+
119
+ _resolver = SchemaResolver()
120
+ _schemas: dict[str, Schema] = {}
121
+
122
+ @classmethod
123
+ def init_instance(cls, schemas_json: dict[str, SchemaValue]) -> None:
124
+ resolver = SchemaResolver(schemas_json)
125
+ schemas = resolver.resolve_all()
126
+ cls._resolver = resolver
127
+ cls._schemas = schemas
128
+
129
+ @classmethod
130
+ def get_instance(cls) -> dict[str, Schema]:
131
+ return dict(cls._schemas)
132
+
133
+
134
+ class Schema:
135
+ def __init__(
136
+ self,
137
+ type: str | list[str],
138
+ description: str | None = None,
139
+ properties: dict[str, Schema] | None = None,
140
+ items: Schema | None = None,
141
+ enum_: list[Any] | None = None,
142
+ required: list[str] | None = None,
143
+ **keywords: Any,
144
+ ) -> None:
145
+ value = {"type": type, **keywords}
146
+ if description is not None:
147
+ value["description"] = description
148
+ if properties is not None:
149
+ value["properties"] = {key: schema.to_json() for key, schema in properties.items()}
150
+ if items is not None:
151
+ value["items"] = items.to_json()
152
+ if enum_ is not None:
153
+ value["enum"] = enum_
154
+ if required is not None:
155
+ value["required"] = required
156
+ self._initialize(value, SchemaResolver())
157
+
158
+ @classmethod
159
+ def from_json(cls, data: SchemaValue, *, resolver: SchemaResolver | None = None) -> Schema:
160
+ schema = cls.__new__(cls)
161
+ schema._initialize(data, resolver if resolver is not None else SchemasSingleton._resolver)
162
+ return schema
163
+
164
+ def _initialize(self, data: SchemaValue, resolver: SchemaResolver) -> None:
165
+ if not isinstance(data, (dict, bool)):
166
+ raise ValueError("Expected a JSON Schema object or boolean")
167
+ self._json = deepcopy(data)
168
+ self._resolver = resolver
169
+ normalized = resolver.normalize(self._json)
170
+ validator_class = validator_for(normalized, default=Draft202012Validator)
171
+ validator_class.check_schema(normalized)
172
+ self._validator = validator_class(normalized, registry=resolver.registry)
173
+ self._format_validator = self._validator.evolve(format_checker=validator_class.FORMAT_CHECKER or FormatChecker())
174
+
175
+ @property
176
+ def is_boolean_schema(self) -> bool:
177
+ return isinstance(self._json, bool)
178
+
179
+ @property
180
+ def raw_schema(self) -> SchemaValue:
181
+ return self.to_json()
182
+
183
+ def __getitem__(self, keyword: str) -> Any:
184
+ if not isinstance(self._json, dict):
185
+ return None
186
+ if keyword in self._json:
187
+ return deepcopy(self._json[keyword])
188
+ return deepcopy(self._resolver.keyword_view(self._json).get(keyword))
189
+
190
+ @property
191
+ def has_type(self) -> bool:
192
+ return self["type"] is not None
193
+
194
+ @property
195
+ def type(self) -> str:
196
+ value = self["type"]
197
+ if isinstance(value, list):
198
+ return value[0] if value else ""
199
+ return value if isinstance(value, str) else ""
200
+
201
+ @property
202
+ def type_union(self) -> list[str] | None:
203
+ value = self["type"]
204
+ return value if isinstance(value, list) else None
205
+
206
+ @property
207
+ def description(self) -> str | None:
208
+ return self["description"]
209
+
210
+ @property
211
+ def enum_(self) -> list[Any] | None:
212
+ return self["enum"]
213
+
214
+ @property
215
+ def required(self) -> list[str] | None:
216
+ return self["required"]
217
+
218
+ @property
219
+ def properties(self) -> dict[str, Schema] | None:
220
+ properties = self["properties"]
221
+ if properties is None:
222
+ return None
223
+ return {key: self._child(node) for key, node in properties.items()}
224
+
225
+ @property
226
+ def items(self) -> Schema | list[Schema] | None:
227
+ items = self["items"]
228
+ if isinstance(items, list):
229
+ return [self._child(node) for node in items]
230
+ return self._child(items) if items is not None else None
231
+
232
+ def _child(self, node: SchemaValue) -> Schema:
233
+ child = self.__class__.__new__(self.__class__)
234
+ child._json = node
235
+ child._resolver = self._resolver
236
+ normalized = self._resolver.normalize(node)
237
+ child._validator = self._validator.evolve(schema=normalized)
238
+ child._format_validator = self._format_validator.evolve(schema=normalized)
239
+ return child
240
+
241
+ def to_json(self) -> SchemaValue:
242
+ return deepcopy(self._json)
243
+
244
+ def validate(self, instance: Any, *, validate_formats: bool = False) -> SchemaValidationResult:
245
+ validator = self._format_validator if validate_formats else self._validator
246
+ errors = [f"{error.json_path}: {error.message}" for error in validator.iter_errors(instance)]
247
+ return SchemaValidationResult(valid=not errors, errors=errors)
248
+
249
+ def is_valid(self, instance: Any, *, validate_formats: bool = False) -> bool:
250
+ validator = self._format_validator if validate_formats else self._validator
251
+ return validator.is_valid(instance)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class ServerConfig:
9
+ url: str
10
+ description: str | None = None
11
+
12
+ @classmethod
13
+ def from_json(cls, data: dict[str, Any]) -> "ServerConfig":
14
+ return cls(url=str(data["url"]), description=data.get("description"))
15
+
16
+ def to_json(self) -> dict[str, Any]:
17
+ payload: dict[str, Any] = {"url": self.url}
18
+ if self.description is not None:
19
+ payload["description"] = self.description
20
+ return payload
@@ -0,0 +1,24 @@
1
+ """Client exports for OpenTool Python SDK."""
2
+
3
+ from .client.exceptions import (
4
+ ErrorNullException,
5
+ OpenToolServerCallException,
6
+ OpenToolServerNoAccessException,
7
+ OpenToolServerUnauthorizedException,
8
+ ResponseNullException,
9
+ )
10
+ from .client.opentool_client import Client, OpenToolClient
11
+ from .dto import * # noqa: F403
12
+ from .llm.model import * # noqa: F403
13
+ from .tool.exceptions import * # noqa: F403
14
+ from .tool.model import * # noqa: F403
15
+
16
+ __all__ = [
17
+ "Client",
18
+ "OpenToolClient",
19
+ "ResponseNullException",
20
+ "ErrorNullException",
21
+ "OpenToolServerUnauthorizedException",
22
+ "OpenToolServerNoAccessException",
23
+ "OpenToolServerCallException",
24
+ ]
@@ -0,0 +1,27 @@
1
+ """Schema exports for OpenTool Python SDK."""
2
+
3
+ from .json_loader import OpenToolJsonLoader
4
+ from .model.data_type import DataType
5
+ from .model.function_model import FunctionModel
6
+ from .model.info import Info
7
+ from .model.opentool import OpenTool
8
+ from .model.parameter import Parameter
9
+ from .model.return_model import Return
10
+ from .model.schema import Schema, SchemaReferenceResolver, SchemaResolver, SchemaType, SchemaValidationResult
11
+ from .model.server import ServerConfig
12
+
13
+ __all__ = [
14
+ "FunctionModel",
15
+ "Info",
16
+ "OpenTool",
17
+ "Parameter",
18
+ "Return",
19
+ "Schema",
20
+ "SchemaType",
21
+ "SchemaReferenceResolver",
22
+ "SchemaResolver",
23
+ "SchemaValidationResult",
24
+ "DataType",
25
+ "ServerConfig",
26
+ "OpenToolJsonLoader",
27
+ ]
@@ -0,0 +1,17 @@
1
+ """Server exports for OpenTool Python SDK."""
2
+
3
+ from .dto import * # noqa: F403
4
+ from .llm.model import * # noqa: F403
5
+ from .opentool_schema import * # noqa: F403
6
+ from .server.cli_arguments import CliArguments
7
+ from .server.opentool_server import OpenToolServer, Server
8
+ from .server.cli_tool import CliTool
9
+ from .tool.exceptions import * # noqa: F403
10
+ from .tool.model import * # noqa: F403
11
+
12
+ __all__ = [
13
+ "Server",
14
+ "OpenToolServer",
15
+ "CliArguments",
16
+ "CliTool",
17
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli_arguments import CliArguments
2
+ from .cli_tool import CliTool
3
+ from .opentool_server import OpenToolServer, Server
4
+
5
+ __all__ = ["CliArguments", "CliTool", "OpenToolServer", "Server"]
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any
5
+
6
+ from ..dto import (
7
+ CLI_ARGUMENT_APIKEYS,
8
+ CLI_ARGUMENT_HOST,
9
+ CLI_ARGUMENT_PORT,
10
+ CLI_ARGUMENT_TAG,
11
+ )
12
+
13
+
14
+ class CliArguments:
15
+ def __init__(self, args: list[str]) -> None:
16
+ self._args = args
17
+ self._parser = argparse.ArgumentParser(add_help=False)
18
+ self._parser.add_argument(f"--{CLI_ARGUMENT_TAG}")
19
+ self._parser.add_argument(f"--{CLI_ARGUMENT_HOST}")
20
+ self._parser.add_argument(f"--{CLI_ARGUMENT_PORT}")
21
+ self._parser.add_argument(f"--{CLI_ARGUMENT_APIKEYS}", action="append")
22
+
23
+ def add_custom_option(self, name: str, **kwargs: Any) -> "CliArguments":
24
+ argument_name = f"--{name}"
25
+ self._parser.add_argument(argument_name, **kwargs)
26
+ return self
27
+
28
+ def add_custom_multi_option(self, name: str, **kwargs: Any) -> "CliArguments":
29
+ argument_name = f"--{name}"
30
+ if "action" not in kwargs:
31
+ kwargs["action"] = "append"
32
+ self._parser.add_argument(argument_name, **kwargs)
33
+ return self
34
+
35
+ def parse(self) -> dict[str, Any]:
36
+ namespace, _ = self._parser.parse_known_args(self._args)
37
+ data: dict[str, Any] = {}
38
+ for key, value in vars(namespace).items():
39
+ if value is not None:
40
+ data[key] = value
41
+ return data
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..tool.tool import Tool
6
+
7
+
8
+ class CliTool(Tool):
9
+ def init_args(self, args: dict[str, Any] | None) -> dict[str, Any] | None:
10
+ return args
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ class ServerException(Exception):
9
+ code: int
10
+ message: str
11
+
12
+ def to_json(self) -> dict[str, Any]:
13
+ return {"code": self.code, "message": self.message}
14
+
15
+
16
+ @dataclass
17
+ class JsonParseException(ServerException):
18
+ message: str = "Parse error"
19
+ code: int = -32700
20
+
21
+ def __str__(self) -> str:
22
+ return json.dumps(self.to_json(), ensure_ascii=False)
23
+
24
+
25
+ @dataclass
26
+ class InvalidJsonPayloadException(ServerException):
27
+ message: str = "Invalid JSON payload: expected object"
28
+ code: int = -32700
29
+
30
+ def __str__(self) -> str:
31
+ return json.dumps(self.to_json(), ensure_ascii=False)