utdawn commited on
Commit
7f24d80
·
verified ·
1 Parent(s): f85a33b

Create tool_declaration_ts.py

Browse files
Files changed (1) hide show
  1. tool_declaration_ts.py +499 -0
tool_declaration_ts.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Encode structured tool declaration to typescript style string.
3
+ """
4
+
5
+ import dataclasses
6
+ import json
7
+ import logging
8
+ from collections.abc import Sequence
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _TS_INDENT = " "
14
+ _TS_FIELD_DELIMITER = ",\n"
15
+
16
+
17
+ class _SchemaRegistry:
18
+ """Registry for schema definitions to handle $ref resolution"""
19
+
20
+ def __init__(self):
21
+ self.definitions = {}
22
+ self.has_self_ref = False
23
+
24
+ def register_definitions(self, defs: dict[str, Any]):
25
+ """Register schema definitions from $defs section"""
26
+ if not defs:
27
+ return
28
+ for def_name, def_schema in defs.items():
29
+ self.definitions[def_name] = def_schema
30
+
31
+ def resolve_ref(self, ref: str) -> dict[str, Any]:
32
+ """Resolve a reference to its schema definition"""
33
+ if ref == "#":
34
+ self.has_self_ref = True
35
+ return {"$self_ref": True}
36
+ elif ref.startswith("#/$defs/"):
37
+ def_name = ref.split("/")[-1]
38
+ if def_name not in self.definitions:
39
+ raise ValueError(f"Reference not found: {ref}")
40
+ return self.definitions[def_name]
41
+ else:
42
+ raise ValueError(f"Unsupported reference format: {ref}")
43
+
44
+
45
+ def _format_description(description: str, indent: str = "") -> str:
46
+ return "\n".join(
47
+ [f"{indent}// {line}" if line else "" for line in description.split("\n")]
48
+ )
49
+
50
+
51
+ class _BaseType:
52
+ description: str
53
+ constraints: dict[str, Any]
54
+
55
+ def __init__(
56
+ self,
57
+ extra_props: dict[str, Any],
58
+ *,
59
+ allowed_constraint_keys: Sequence[str] = (),
60
+ ):
61
+ self.description = extra_props.get("description", "")
62
+ self.constraints = {
63
+ k: v for k, v in extra_props.items() if k in allowed_constraint_keys
64
+ }
65
+
66
+ def to_typescript_style(self, indent: str = "") -> str:
67
+ raise NotImplementedError
68
+
69
+ def format_docstring(self, indent: str) -> str:
70
+ lines = []
71
+ if self.description:
72
+ lines.append(_format_description(self.description, indent))
73
+ if self.constraints:
74
+ constraints_str = ", ".join(
75
+ f"{k}: {v}"
76
+ for k, v in sorted(self.constraints.items(), key=lambda kv: kv[0])
77
+ )
78
+ lines.append(f"{indent}// {constraints_str}")
79
+
80
+ return "".join(x + "\n" for x in lines)
81
+
82
+
83
+ class _ParameterTypeScalar(_BaseType):
84
+ type: str
85
+
86
+ def __init__(self, type: str, extra_props: dict[str, Any] | None = None):
87
+ self.type = type
88
+
89
+ allowed_constraint_keys: list[str] = []
90
+ if self.type == "string":
91
+ allowed_constraint_keys = ["maxLength", "minLength", "pattern"]
92
+ elif self.type in ("number", "integer"):
93
+ allowed_constraint_keys = ["maximum", "minimum"]
94
+
95
+ super().__init__(
96
+ extra_props or {}, allowed_constraint_keys=allowed_constraint_keys
97
+ )
98
+
99
+ def to_typescript_style(self, indent: str = "") -> str:
100
+ # Map integer to number in TypeScript
101
+ if self.type == "integer":
102
+ return "number"
103
+ return self.type
104
+
105
+
106
+ class _ParameterTypeObject(_BaseType):
107
+ properties: list["_Parameter"]
108
+ additional_properties: Any | None = None
109
+
110
+ def __init__(
111
+ self,
112
+ json_schema_object: dict[str, Any],
113
+ registry: _SchemaRegistry | None = None,
114
+ ):
115
+ super().__init__(json_schema_object)
116
+
117
+ self.properties = []
118
+ self.additional_properties = None
119
+
120
+ if not json_schema_object:
121
+ return
122
+
123
+ if "$defs" in json_schema_object and registry:
124
+ registry.register_definitions(json_schema_object["$defs"])
125
+
126
+ self.additional_properties = json_schema_object.get("additionalProperties")
127
+ if isinstance(self.additional_properties, dict):
128
+ self.additional_properties = _parse_parameter_type(
129
+ self.additional_properties, registry
130
+ )
131
+
132
+ if "properties" not in json_schema_object:
133
+ return
134
+
135
+ required_parameters = json_schema_object.get("required", [])
136
+ optional_parameters = set(json_schema_object["properties"].keys()) - set(
137
+ required_parameters
138
+ )
139
+
140
+ self.properties = [
141
+ _Parameter(
142
+ name=name,
143
+ type=_parse_parameter_type(prop, registry),
144
+ optional=name in optional_parameters,
145
+ default=prop.get("default") if isinstance(prop, dict) else None,
146
+ )
147
+ for name, prop in json_schema_object["properties"].items()
148
+ ]
149
+
150
+ def to_typescript_style(self, indent: str = "") -> str:
151
+ # sort by optional, make the required parameters first
152
+ parameters = [p for p in self.properties if not p.optional]
153
+ opt_params = [p for p in self.properties if p.optional]
154
+
155
+ parameters = sorted(parameters, key=lambda p: p.name)
156
+ parameters.extend(sorted(opt_params, key=lambda p: p.name))
157
+
158
+ param_strs = []
159
+ for p in parameters:
160
+ one = p.to_typescript_style(indent=indent + _TS_INDENT)
161
+ param_strs.append(one)
162
+
163
+ if self.additional_properties is not None:
164
+ ap_type_str = "any"
165
+ if self.additional_properties is True:
166
+ ap_type_str = "any"
167
+ elif self.additional_properties is False:
168
+ ap_type_str = "never"
169
+ elif isinstance(self.additional_properties, _ParameterType):
170
+ ap_type_str = self.additional_properties.to_typescript_style(
171
+ indent=indent + _TS_INDENT
172
+ )
173
+ else:
174
+ raise ValueError(
175
+ f"Unknown additionalProperties: {self.additional_properties}"
176
+ )
177
+ param_strs.append(f"{indent + _TS_INDENT}[k: string]: {ap_type_str}")
178
+
179
+ if not param_strs:
180
+ return "{}"
181
+
182
+ params_str = _TS_FIELD_DELIMITER.join(param_strs)
183
+ if params_str:
184
+ # add new line before and after
185
+ params_str = f"\n{params_str}\n"
186
+ # always wrap with object
187
+ return f"{{{params_str}{indent}}}"
188
+
189
+
190
+ class _ParameterTypeArray(_BaseType):
191
+ item: "_ParameterType"
192
+
193
+ def __init__(
194
+ self,
195
+ json_schema_object: dict[str, Any],
196
+ registry: _SchemaRegistry | None = None,
197
+ ):
198
+ super().__init__(
199
+ json_schema_object, allowed_constraint_keys=("minItems", "maxItems")
200
+ )
201
+ if json_schema_object.get("items"):
202
+ self.item = _parse_parameter_type(json_schema_object["items"], registry)
203
+ else:
204
+ self.item = _ParameterTypeScalar(type="any")
205
+
206
+ def to_typescript_style(self, indent: str = "") -> str:
207
+ item_docstring = self.item.format_docstring(indent + _TS_INDENT)
208
+ if item_docstring:
209
+ return (
210
+ "Array<\n"
211
+ + item_docstring
212
+ + indent
213
+ + _TS_INDENT
214
+ + self.item.to_typescript_style(indent=indent + _TS_INDENT)
215
+ + "\n"
216
+ + indent
217
+ + ">"
218
+ )
219
+ else:
220
+ return f"Array<{self.item.to_typescript_style(indent=indent)}>"
221
+
222
+
223
+ class _ParameterTypeEnum(_BaseType):
224
+ # support scalar types only
225
+ enum: list[str | int | float | bool | None]
226
+
227
+ def __init__(self, json_schema_object: dict[str, Any]):
228
+ super().__init__(json_schema_object)
229
+ self.enum = json_schema_object["enum"]
230
+
231
+ # Validate enum values against declared type if present
232
+ if "type" in json_schema_object:
233
+ typ = json_schema_object["type"]
234
+ if isinstance(typ, list):
235
+ if len(typ) == 1:
236
+ typ = typ[0]
237
+ elif len(typ) == 2:
238
+ if "null" not in typ:
239
+ raise ValueError(f"Enum type {typ} is not supported")
240
+ else:
241
+ typ = typ[0] if typ[0] != "null" else typ[1]
242
+ else:
243
+ raise ValueError(f"Enum type {typ} is not supported")
244
+ for val in self.enum:
245
+ if val is None:
246
+ continue
247
+ if typ == "string" and not isinstance(val, str):
248
+ raise ValueError(f"Enum value {val} is not a string")
249
+ elif typ == "number" and not isinstance(val, (int, float)):
250
+ raise ValueError(f"Enum value {val} is not a number")
251
+ elif typ == "integer" and not isinstance(val, int):
252
+ raise ValueError(f"Enum value {val} is not an integer")
253
+ elif typ == "boolean" and not isinstance(val, bool):
254
+ raise ValueError(f"Enum value {val} is not a boolean")
255
+
256
+ def to_typescript_style(self, indent: str = "") -> str:
257
+ return " | ".join(
258
+ [f'"{e}"' if isinstance(e, str) else str(e) for e in self.enum]
259
+ )
260
+
261
+
262
+ class _ParameterTypeAnyOf(_BaseType):
263
+ types: list["_ParameterType"]
264
+
265
+ def __init__(
266
+ self,
267
+ json_schema_object: dict[str, Any],
268
+ registry: _SchemaRegistry | None = None,
269
+ ):
270
+ super().__init__(json_schema_object)
271
+ self.types = [
272
+ _parse_parameter_type(t, registry) for t in json_schema_object["anyOf"]
273
+ ]
274
+
275
+ def to_typescript_style(self, indent: str = "") -> str:
276
+ return " | ".join([t.to_typescript_style(indent=indent) for t in self.types])
277
+
278
+
279
+ class _ParameterTypeUnion(_BaseType):
280
+ types: list[str]
281
+
282
+ def __init__(self, json_schema_object: dict[str, Any]):
283
+ super().__init__(json_schema_object)
284
+
285
+ mapping = {
286
+ "string": "string",
287
+ "number": "number",
288
+ "integer": "number",
289
+ "boolean": "boolean",
290
+ "null": "null",
291
+ "object": "{}",
292
+ "array": "Array<any>",
293
+ }
294
+ self.types = [mapping[t] for t in json_schema_object["type"]]
295
+
296
+ def to_typescript_style(self, indent: str = "") -> str:
297
+ return " | ".join(self.types)
298
+
299
+
300
+ class _ParameterTypeRef(_BaseType):
301
+ ref_name: str
302
+ is_self_ref: bool = False
303
+
304
+ def __init__(self, json_schema_object: dict[str, Any], registry: _SchemaRegistry):
305
+ super().__init__(json_schema_object)
306
+
307
+ ref = json_schema_object["$ref"]
308
+ resolved_schema = registry.resolve_ref(ref)
309
+
310
+ if resolved_schema.get("$self_ref", False):
311
+ self.ref_name = "parameters"
312
+ self.is_self_ref = True
313
+ else:
314
+ self.ref_name = ref.split("/")[-1]
315
+
316
+ def to_typescript_style(self, indent: str = "") -> str:
317
+ return self.ref_name
318
+
319
+
320
+ _ParameterType = (
321
+ _ParameterTypeScalar
322
+ | _ParameterTypeObject
323
+ | _ParameterTypeArray
324
+ | _ParameterTypeEnum
325
+ | _ParameterTypeAnyOf
326
+ | _ParameterTypeUnion
327
+ | _ParameterTypeRef
328
+ )
329
+
330
+
331
+ @dataclasses.dataclass
332
+ class _Parameter:
333
+ """
334
+ A parameter in a function, or a field in a object.
335
+ It consists of the type as well as the name.
336
+ """
337
+
338
+ type: _ParameterType
339
+ name: str = "_"
340
+ optional: bool = True
341
+ default: Any | None = None
342
+
343
+ @classmethod
344
+ def parse_extended(cls, attributes: dict[str, Any]) -> "_Parameter":
345
+ if not attributes:
346
+ raise ValueError("attributes is empty")
347
+
348
+ return cls(
349
+ name=attributes.get("name", "_"),
350
+ type=_parse_parameter_type(attributes),
351
+ optional=attributes.get("optional", False),
352
+ default=attributes.get("default"),
353
+ )
354
+
355
+ def to_typescript_style(self, indent: str = "") -> str:
356
+ comments = self.type.format_docstring(indent)
357
+
358
+ if self.default is not None:
359
+ default_repr = (
360
+ json.dumps(self.default, ensure_ascii=False)
361
+ if not isinstance(self.default, (int, float, bool))
362
+ else repr(self.default)
363
+ )
364
+ comments += f"{indent}// Default: {default_repr}\n"
365
+
366
+ return (
367
+ comments
368
+ + f"{indent}{self.name}{'?' if self.optional else ''}: {self.type.to_typescript_style(indent=indent)}"
369
+ )
370
+
371
+
372
+ def _parse_parameter_type(
373
+ json_schema_object: dict[str, Any] | bool, registry: _SchemaRegistry | None = None
374
+ ) -> _ParameterType:
375
+ if isinstance(json_schema_object, bool):
376
+ if json_schema_object:
377
+ return _ParameterTypeScalar(type="any")
378
+ else:
379
+ logger.warning(
380
+ f"Warning: Boolean value {json_schema_object} is not supported, use null instead."
381
+ )
382
+ return _ParameterTypeScalar(type="null")
383
+
384
+ if "$ref" in json_schema_object and registry:
385
+ return _ParameterTypeRef(json_schema_object, registry)
386
+
387
+ if "anyOf" in json_schema_object:
388
+ return _ParameterTypeAnyOf(json_schema_object, registry)
389
+ elif "enum" in json_schema_object:
390
+ return _ParameterTypeEnum(json_schema_object)
391
+ elif "type" in json_schema_object:
392
+ typ = json_schema_object["type"]
393
+ if isinstance(typ, list):
394
+ return _ParameterTypeUnion(json_schema_object)
395
+ elif typ == "object":
396
+ return _ParameterTypeObject(json_schema_object, registry)
397
+ elif typ == "array":
398
+ return _ParameterTypeArray(json_schema_object, registry)
399
+ else:
400
+ return _ParameterTypeScalar(typ, json_schema_object)
401
+ elif json_schema_object == {}:
402
+ return _ParameterTypeScalar(type="any")
403
+ else:
404
+ raise ValueError(f"Invalid JSON Schema object: {json_schema_object}")
405
+
406
+
407
+ def _openai_function_to_typescript_style(
408
+ function: dict[str, Any],
409
+ ) -> str:
410
+ """Convert OpenAI function definition (dict) to TypeScript style string."""
411
+ registry = _SchemaRegistry()
412
+ parameters = function.get("parameters") or {}
413
+ parsed = _ParameterTypeObject(parameters, registry)
414
+
415
+ interfaces = []
416
+ root_interface_name = None
417
+ if registry.has_self_ref:
418
+ root_interface_name = "parameters"
419
+ params_str = _TS_FIELD_DELIMITER.join(
420
+ [p.to_typescript_style(indent=_TS_INDENT) for p in parsed.properties]
421
+ )
422
+ params_str = f"\n{params_str}\n" if params_str else ""
423
+ interface_def = f"interface {root_interface_name} {{{params_str}}}"
424
+ interfaces.append(interface_def)
425
+
426
+ definitions_copy = dict(registry.definitions)
427
+ for def_name, def_schema in definitions_copy.items():
428
+ obj_type = _parse_parameter_type(def_schema, registry)
429
+ params_str = obj_type.to_typescript_style()
430
+
431
+ description_part = ""
432
+ if obj_description := def_schema.get("description", ""):
433
+ description_part = _format_description(obj_description) + "\n"
434
+
435
+ interface_def = f"{description_part}interface {def_name} {params_str}"
436
+ interfaces.append(interface_def)
437
+
438
+ interface_str = "\n".join(interfaces)
439
+ function_name = function.get("name", "function")
440
+ if root_interface_name:
441
+ type_def = f"type {function_name} = (_: {root_interface_name}) => any;"
442
+ else:
443
+ params_str = parsed.to_typescript_style()
444
+ type_def = f"type {function_name} = (_: {params_str}) => any;"
445
+
446
+ description = function.get("description")
447
+ return "\n".join(
448
+ filter(
449
+ bool,
450
+ [
451
+ interface_str,
452
+ ((description and _format_description(description)) or ""),
453
+ type_def,
454
+ ],
455
+ )
456
+ )
457
+
458
+
459
+ def encode_tools_to_typescript_style(
460
+ tools: list[dict[str, Any]],
461
+ ) -> str:
462
+ """
463
+ Convert tools (list of dict) to TypeScript style string.
464
+
465
+ Supports OpenAI format: {"type": "function", "function": {...}}
466
+
467
+ Args:
468
+ tools: List of tool definitions in dict format
469
+
470
+ Returns:
471
+ TypeScript style string representation of the tools
472
+ """
473
+ if not tools:
474
+ return ""
475
+
476
+ functions = []
477
+
478
+ for tool in tools:
479
+ tool_type = tool.get("type")
480
+ if tool_type == "function":
481
+ func_def = tool.get("function", {})
482
+ if func_def:
483
+ functions.append(_openai_function_to_typescript_style(func_def))
484
+ else:
485
+ # Skip unsupported tool types (like "_plugin")
486
+ continue
487
+
488
+ if not functions:
489
+ return ""
490
+
491
+ functions_str = "\n".join(functions)
492
+ result = "# Tools\n\n"
493
+
494
+ if functions_str:
495
+ result += "## functions\nnamespace functions {\n"
496
+ result += functions_str + "\n"
497
+ result += "}\n"
498
+
499
+ return result