qtype 0.0.12__py3-none-any.whl → 0.0.13__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.
- qtype/application/converters/tools_from_api.py +476 -11
- qtype/application/converters/tools_from_module.py +37 -13
- qtype/application/converters/types.py +14 -0
- qtype/application/facade.py +17 -20
- qtype/commands/convert.py +36 -2
- qtype/commands/generate.py +48 -0
- qtype/commands/run.py +1 -0
- qtype/commands/serve.py +11 -1
- qtype/commands/validate.py +8 -11
- qtype/commands/visualize.py +0 -3
- qtype/dsl/model.py +190 -4
- qtype/dsl/validator.py +2 -1
- qtype/interpreter/api.py +5 -1
- qtype/interpreter/batch/file_sink_source.py +162 -0
- qtype/interpreter/batch/flow.py +1 -1
- qtype/interpreter/batch/sql_source.py +3 -6
- qtype/interpreter/batch/step.py +12 -1
- qtype/interpreter/batch/utils.py +8 -9
- qtype/interpreter/step.py +2 -2
- qtype/interpreter/steps/tool.py +194 -28
- qtype/interpreter/ui/404/index.html +1 -1
- qtype/interpreter/ui/404.html +1 -1
- qtype/interpreter/ui/_next/static/chunks/393-8fd474427f8e19ce.js +36 -0
- qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
- qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +3 -0
- qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +4 -4
- qtype/loader.py +2 -1
- qtype/semantic/generate.py +6 -2
- qtype/semantic/model.py +132 -77
- qtype/semantic/visualize.py +24 -6
- {qtype-0.0.12.dist-info → qtype-0.0.13.dist-info}/METADATA +4 -2
- {qtype-0.0.12.dist-info → qtype-0.0.13.dist-info}/RECORD +44 -43
- qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
- qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
- qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
- qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
- /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → nUaw6_IwRwPqkzwe5s725}/_buildManifest.js +0 -0
- /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → nUaw6_IwRwPqkzwe5s725}/_ssgManifest.js +0 -0
- {qtype-0.0.12.dist-info → qtype-0.0.13.dist-info}/WHEEL +0 -0
- {qtype-0.0.12.dist-info → qtype-0.0.13.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.12.dist-info → qtype-0.0.13.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.12.dist-info → qtype-0.0.13.dist-info}/top_level.txt +0 -0
|
@@ -1,24 +1,489 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from openapi_parser import parse
|
|
6
|
+
from openapi_parser.enumeration import (
|
|
7
|
+
AuthenticationScheme,
|
|
8
|
+
DataType,
|
|
9
|
+
SecurityType,
|
|
10
|
+
)
|
|
11
|
+
from openapi_parser.specification import Array, Content, Object, Operation
|
|
12
|
+
from openapi_parser.specification import Path as OAPIPath
|
|
13
|
+
from openapi_parser.specification import (
|
|
14
|
+
RequestBody,
|
|
15
|
+
Response,
|
|
16
|
+
Schema,
|
|
17
|
+
Security,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from qtype.dsl.base_types import PrimitiveTypeEnum
|
|
21
|
+
from qtype.dsl.model import (
|
|
22
|
+
APIKeyAuthProvider,
|
|
23
|
+
APITool,
|
|
24
|
+
AuthorizationProvider,
|
|
25
|
+
AuthProviderType,
|
|
26
|
+
BearerTokenAuthProvider,
|
|
27
|
+
CustomType,
|
|
28
|
+
OAuth2AuthProvider,
|
|
29
|
+
ToolParameter,
|
|
30
|
+
VariableType,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _schema_to_qtype_properties(
|
|
35
|
+
schema: Schema,
|
|
36
|
+
existing_custom_types: dict[str, CustomType],
|
|
37
|
+
schema_name_map: dict[int, str],
|
|
38
|
+
) -> dict[str, str]:
|
|
39
|
+
"""Convert OpenAPI Schema properties to QType CustomType properties."""
|
|
40
|
+
properties = {}
|
|
41
|
+
|
|
42
|
+
# Check if schema is an Object type with properties
|
|
43
|
+
if isinstance(schema, Object) and schema.properties:
|
|
44
|
+
# Get the list of required properties for this object
|
|
45
|
+
required_props = schema.required or []
|
|
46
|
+
|
|
47
|
+
for prop in schema.properties:
|
|
48
|
+
prop_type = _schema_to_qtype_type(
|
|
49
|
+
prop.schema, existing_custom_types, schema_name_map
|
|
50
|
+
)
|
|
51
|
+
# Convert to string representation for storage in properties dict
|
|
52
|
+
prop_type_str = _type_to_string(prop_type)
|
|
53
|
+
|
|
54
|
+
# Add '?' suffix for optional properties (not in required list)
|
|
55
|
+
if prop.name not in required_props:
|
|
56
|
+
prop_type_str += "?"
|
|
57
|
+
|
|
58
|
+
properties[prop.name] = prop_type_str
|
|
59
|
+
else:
|
|
60
|
+
# For non-object schemas, create a default property
|
|
61
|
+
default_type = _schema_to_qtype_type(
|
|
62
|
+
schema, existing_custom_types, schema_name_map
|
|
63
|
+
)
|
|
64
|
+
default_type_str = _type_to_string(default_type)
|
|
65
|
+
properties["value"] = default_type_str
|
|
66
|
+
|
|
67
|
+
return properties
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _type_to_string(qtype: PrimitiveTypeEnum | CustomType | str | type) -> str:
|
|
71
|
+
"""Convert a QType to its string representation."""
|
|
72
|
+
if isinstance(qtype, PrimitiveTypeEnum):
|
|
73
|
+
return qtype.value
|
|
74
|
+
elif isinstance(qtype, CustomType):
|
|
75
|
+
return qtype.id
|
|
76
|
+
elif isinstance(qtype, type):
|
|
77
|
+
# Handle domain types like ChatMessage, Embedding, etc.
|
|
78
|
+
return qtype.__name__
|
|
79
|
+
else:
|
|
80
|
+
return str(qtype)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _create_custom_type_from_schema(
|
|
84
|
+
schema: Schema,
|
|
85
|
+
existing_custom_types: dict[str, CustomType],
|
|
86
|
+
schema_name_map: dict[int, str],
|
|
87
|
+
) -> CustomType:
|
|
88
|
+
"""Create a CustomType from an Object schema."""
|
|
89
|
+
# Generate a unique ID for this schema-based type
|
|
90
|
+
type_id = None
|
|
91
|
+
|
|
92
|
+
schema_hash = hash(str(schema))
|
|
93
|
+
if schema_hash in schema_name_map:
|
|
94
|
+
type_id = schema_name_map[schema_hash]
|
|
95
|
+
else:
|
|
96
|
+
# make a type id manually
|
|
97
|
+
if schema.title:
|
|
98
|
+
# Use title if available, make it lowercase, alphanumeric, snake_case
|
|
99
|
+
base_id = schema.title.lower().replace(" ", "_").replace("-", "_")
|
|
100
|
+
# Remove non-alphanumeric characters except underscores
|
|
101
|
+
type_id = "schema_" + "".join(
|
|
102
|
+
c for c in base_id if c.isalnum() or c == "_"
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
# Fallback to hash if no title
|
|
106
|
+
type_id = f"schema_{hash(str(schema))}"
|
|
107
|
+
|
|
108
|
+
# Check if we already have this type
|
|
109
|
+
if type_id in existing_custom_types:
|
|
110
|
+
return existing_custom_types[type_id]
|
|
111
|
+
|
|
112
|
+
# Create properties from the schema
|
|
113
|
+
properties = _schema_to_qtype_properties(
|
|
114
|
+
schema, existing_custom_types, schema_name_map
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Create the custom type
|
|
118
|
+
custom_type = CustomType(
|
|
119
|
+
id=type_id,
|
|
120
|
+
description=schema.description
|
|
121
|
+
or schema.title
|
|
122
|
+
or "Generated from OpenAPI schema",
|
|
123
|
+
properties=properties,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Store it in the registry to prevent infinite recursion
|
|
127
|
+
existing_custom_types[type_id] = custom_type
|
|
128
|
+
|
|
129
|
+
return custom_type
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _schema_to_qtype_type(
|
|
133
|
+
schema: Schema,
|
|
134
|
+
existing_custom_types: dict[str, CustomType],
|
|
135
|
+
schema_name_map: dict[int, str],
|
|
136
|
+
) -> PrimitiveTypeEnum | CustomType | str:
|
|
137
|
+
"""Recursively convert OpenAPI Schema to QType, handling nested types."""
|
|
138
|
+
match schema.type:
|
|
139
|
+
case DataType.STRING:
|
|
140
|
+
return PrimitiveTypeEnum.text
|
|
141
|
+
case DataType.INTEGER:
|
|
142
|
+
return PrimitiveTypeEnum.int
|
|
143
|
+
case DataType.NUMBER:
|
|
144
|
+
return PrimitiveTypeEnum.float
|
|
145
|
+
case DataType.BOOLEAN:
|
|
146
|
+
return PrimitiveTypeEnum.boolean
|
|
147
|
+
case DataType.ARRAY:
|
|
148
|
+
if isinstance(schema, Array) and schema.items:
|
|
149
|
+
item_type = _schema_to_qtype_type(
|
|
150
|
+
schema.items, existing_custom_types, schema_name_map
|
|
151
|
+
)
|
|
152
|
+
item_type_str = _type_to_string(item_type)
|
|
153
|
+
return f"list[{item_type_str}]"
|
|
154
|
+
return "list[text]" # Default to list of text when no item type is specified
|
|
155
|
+
case DataType.OBJECT:
|
|
156
|
+
# For object types, create a custom type
|
|
157
|
+
return _create_custom_type_from_schema(
|
|
158
|
+
schema, existing_custom_types, schema_name_map
|
|
159
|
+
)
|
|
160
|
+
case DataType.NULL:
|
|
161
|
+
return PrimitiveTypeEnum.text # Default to text for null types
|
|
162
|
+
case _:
|
|
163
|
+
return PrimitiveTypeEnum.text # Default fallback
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def to_variable_type(
|
|
167
|
+
content: Content,
|
|
168
|
+
existing_custom_types: dict[str, CustomType],
|
|
169
|
+
schema_name_map: dict[int, str],
|
|
170
|
+
) -> VariableType | CustomType:
|
|
171
|
+
"""
|
|
172
|
+
Convert an OpenAPI Content object to a VariableType or CustomType.
|
|
173
|
+
If it already exists in existing_custom_types, return that instance.
|
|
174
|
+
"""
|
|
175
|
+
# Check if we have a schema to analyze
|
|
176
|
+
if not content.schema:
|
|
177
|
+
return PrimitiveTypeEnum.text
|
|
178
|
+
|
|
179
|
+
# Use the recursive schema conversion function
|
|
180
|
+
result = _schema_to_qtype_type(
|
|
181
|
+
content.schema, existing_custom_types, schema_name_map
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# If it's a string (like "list[text]"), we need to return it as-is for now
|
|
185
|
+
# The semantic layer will handle string-based type references
|
|
186
|
+
if isinstance(result, str):
|
|
187
|
+
# For now, return as text since we can't directly represent complex string types
|
|
188
|
+
# in VariableType union. The semantic resolver will handle this.
|
|
189
|
+
return PrimitiveTypeEnum.text
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_tool_parameters_from_body(
|
|
195
|
+
oas: Response | RequestBody,
|
|
196
|
+
existing_custom_types: dict[str, CustomType],
|
|
197
|
+
schema_name_map: dict[int, str],
|
|
198
|
+
default_param_name: str,
|
|
199
|
+
) -> dict[str, ToolParameter]:
|
|
200
|
+
"""
|
|
201
|
+
Convert an OpenAPI Response or RequestBody to a dictionary of ToolParameters.
|
|
202
|
+
|
|
203
|
+
If the body has only one content type with an Object schema, flatten its properties
|
|
204
|
+
to individual parameters. Otherwise, create a single parameter with the body type.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
oas: The OpenAPI Response or RequestBody object
|
|
208
|
+
existing_custom_types: Dictionary of existing custom types
|
|
209
|
+
schema_name_map: Mapping from schema hash to name
|
|
210
|
+
default_param_name: Name to use for non-flattened parameter
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dictionary of parameter name to ToolParameter objects
|
|
214
|
+
"""
|
|
215
|
+
# Check if we have content to analyze
|
|
216
|
+
if not hasattr(oas, "content") or not oas.content:
|
|
217
|
+
return {}
|
|
218
|
+
|
|
219
|
+
content = oas.content[0]
|
|
220
|
+
input_type = to_variable_type(
|
|
221
|
+
content, existing_custom_types, schema_name_map
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Convert CustomType to string ID for ToolParameter
|
|
225
|
+
input_type_value = (
|
|
226
|
+
input_type.id if isinstance(input_type, CustomType) else input_type
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Check if we should flatten: if this is a CustomType that exists
|
|
230
|
+
if (
|
|
231
|
+
isinstance(input_type, CustomType)
|
|
232
|
+
and input_type.id in existing_custom_types
|
|
233
|
+
):
|
|
234
|
+
custom_type = existing_custom_types[input_type.id]
|
|
235
|
+
|
|
236
|
+
# Flatten the custom type properties to individual parameters
|
|
237
|
+
flattened_parameters = {}
|
|
238
|
+
for prop_name, prop_type_str in custom_type.properties.items():
|
|
239
|
+
# Check if the property is optional (has '?' suffix)
|
|
240
|
+
is_optional = prop_type_str.endswith("?")
|
|
241
|
+
clean_type = (
|
|
242
|
+
prop_type_str.rstrip("?") if is_optional else prop_type_str
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
flattened_parameters[prop_name] = ToolParameter(
|
|
246
|
+
type=clean_type, optional=is_optional
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# remove the type from existing_custom_types to avoid confusion
|
|
250
|
+
del existing_custom_types[input_type.id]
|
|
251
|
+
|
|
252
|
+
return flattened_parameters
|
|
253
|
+
|
|
254
|
+
# If not flattening, create a single parameter (e.g., for simple types or arrays)
|
|
255
|
+
return {
|
|
256
|
+
default_param_name: ToolParameter(
|
|
257
|
+
type=input_type_value, optional=False
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def to_api_tool(
|
|
263
|
+
server_url: str,
|
|
264
|
+
auth: Optional[AuthorizationProvider],
|
|
265
|
+
path: OAPIPath,
|
|
266
|
+
operation: Operation,
|
|
267
|
+
existing_custom_types: dict[str, CustomType],
|
|
268
|
+
schema_name_map: dict[int, str],
|
|
269
|
+
) -> APITool:
|
|
270
|
+
"""Convert an OpenAPI Path and Operation to a Tool."""
|
|
271
|
+
endpoint = server_url.rstrip("/") + path.url
|
|
272
|
+
|
|
273
|
+
# Generate a unique ID for this tool
|
|
274
|
+
tool_id = (
|
|
275
|
+
operation.operation_id
|
|
276
|
+
or f"{operation.method.value}_{path.url.replace('/', '_').replace('{', '').replace('}', '')}"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Use operation summary as name, fallback to operation_id or generated name
|
|
280
|
+
tool_name = (
|
|
281
|
+
operation.summary
|
|
282
|
+
or operation.operation_id
|
|
283
|
+
or f"{operation.method.value.upper()} {path.url}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Use operation description, fallback to summary or generated description
|
|
287
|
+
tool_description = (
|
|
288
|
+
operation.description
|
|
289
|
+
or operation.summary
|
|
290
|
+
or f"API call to {operation.method.value.upper()} {path.url}"
|
|
291
|
+
).replace("\n", " ")
|
|
292
|
+
|
|
293
|
+
# Process inputs from request body and parameters
|
|
294
|
+
inputs = {}
|
|
295
|
+
if operation.request_body and operation.request_body.content:
|
|
296
|
+
# Create input parameters from request body using the new function
|
|
297
|
+
input_params = create_tool_parameters_from_body(
|
|
298
|
+
operation.request_body,
|
|
299
|
+
existing_custom_types,
|
|
300
|
+
schema_name_map,
|
|
301
|
+
default_param_name="request",
|
|
302
|
+
)
|
|
303
|
+
inputs.update(input_params)
|
|
304
|
+
|
|
305
|
+
# Add path and query parameters as inputs
|
|
306
|
+
parameters = {}
|
|
307
|
+
for param in operation.parameters:
|
|
308
|
+
if param.schema:
|
|
309
|
+
param_type = _schema_to_qtype_type(
|
|
310
|
+
param.schema, existing_custom_types, schema_name_map
|
|
311
|
+
)
|
|
312
|
+
# Convert to appropriate type for ToolParameter
|
|
313
|
+
param_type_value = (
|
|
314
|
+
param_type.id
|
|
315
|
+
if isinstance(param_type, CustomType)
|
|
316
|
+
else param_type
|
|
317
|
+
)
|
|
318
|
+
parameters[param.name] = ToolParameter(
|
|
319
|
+
type=param_type_value, optional=not param.required
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Process outputs from responses
|
|
323
|
+
outputs = {}
|
|
324
|
+
# Find the success response (200-299 status codes) or default response
|
|
325
|
+
success_response = next(
|
|
326
|
+
(r for r in operation.responses if r.code and 200 <= r.code < 300),
|
|
327
|
+
next((r for r in operation.responses if r.is_default), None),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# If we found a success response, create output parameters
|
|
331
|
+
if success_response and success_response.content:
|
|
332
|
+
output_params = create_tool_parameters_from_body(
|
|
333
|
+
success_response,
|
|
334
|
+
existing_custom_types,
|
|
335
|
+
schema_name_map,
|
|
336
|
+
default_param_name="response",
|
|
337
|
+
)
|
|
338
|
+
outputs.update(output_params)
|
|
339
|
+
|
|
340
|
+
return APITool(
|
|
341
|
+
id=tool_id,
|
|
342
|
+
name=tool_name,
|
|
343
|
+
description=tool_description,
|
|
344
|
+
endpoint=endpoint,
|
|
345
|
+
method=operation.method.value.upper(),
|
|
346
|
+
auth=auth.id if auth else None, # Use auth ID string instead of object
|
|
347
|
+
inputs=inputs if inputs else None,
|
|
348
|
+
outputs=outputs if outputs else None,
|
|
349
|
+
parameters=parameters if parameters else None,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def to_authorization_provider(
|
|
354
|
+
api_name: str, scheme_name: str, security: Security
|
|
355
|
+
) -> AuthProviderType:
|
|
356
|
+
if security.scheme is None:
|
|
357
|
+
raise ValueError("Security scheme is missing")
|
|
358
|
+
|
|
359
|
+
match security.type:
|
|
360
|
+
case SecurityType.API_KEY:
|
|
361
|
+
return APIKeyAuthProvider(
|
|
362
|
+
id=f"{api_name}_{scheme_name}_{security.name or 'api_key'}",
|
|
363
|
+
api_key="your_api_key_here", # User will need to configure
|
|
364
|
+
host=None, # Will be set from base URL if available
|
|
365
|
+
)
|
|
366
|
+
case SecurityType.HTTP:
|
|
367
|
+
if security.scheme == AuthenticationScheme.BEARER:
|
|
368
|
+
return BearerTokenAuthProvider(
|
|
369
|
+
id=f"{api_name}_{scheme_name}_{security.bearer_format or 'token'}",
|
|
370
|
+
token=f"${{{api_name.upper()}_BEARER}}", # User will need to configure
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"HTTP authentication scheme '{security.scheme}' is not supported"
|
|
375
|
+
)
|
|
376
|
+
case SecurityType.OAUTH2:
|
|
377
|
+
return OAuth2AuthProvider(
|
|
378
|
+
id=f"{api_name}_{scheme_name}_{hash(str(security.flows))}",
|
|
379
|
+
client_id="your_client_id", # User will need to configure
|
|
380
|
+
client_secret="your_client_secret", # User will need to configure
|
|
381
|
+
token_url=next(
|
|
382
|
+
(
|
|
383
|
+
flow.token_url
|
|
384
|
+
for flow in security.flows.values()
|
|
385
|
+
if flow.token_url
|
|
386
|
+
),
|
|
387
|
+
"https://example.com/oauth/token", # Default fallback
|
|
388
|
+
),
|
|
389
|
+
scopes=list(
|
|
390
|
+
{
|
|
391
|
+
scope
|
|
392
|
+
for flow in security.flows.values()
|
|
393
|
+
for scope in flow.scopes.keys()
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
if any(flow.scopes for flow in security.flows.values())
|
|
397
|
+
else None,
|
|
398
|
+
)
|
|
399
|
+
case _:
|
|
400
|
+
raise ValueError(
|
|
401
|
+
f"Security type '{security.type}' is not supported"
|
|
402
|
+
)
|
|
2
403
|
|
|
3
404
|
|
|
4
405
|
def tools_from_api(
|
|
5
406
|
openapi_spec: str,
|
|
6
|
-
|
|
7
|
-
include_tags: list[str] | None = None,
|
|
8
|
-
auth: AuthorizationProvider | str | None = None,
|
|
9
|
-
) -> list[APITool]:
|
|
407
|
+
) -> tuple[str, list[AuthProviderType], list[APITool], list[CustomType]]:
|
|
10
408
|
"""
|
|
11
|
-
|
|
409
|
+
Creates tools from an OpenAPI specification.
|
|
12
410
|
|
|
13
411
|
Args:
|
|
14
|
-
|
|
412
|
+
openapi_spec: The OpenAPI specification path or URL.
|
|
15
413
|
|
|
16
414
|
Returns:
|
|
17
|
-
|
|
415
|
+
Tuple containing:
|
|
416
|
+
- API name
|
|
417
|
+
- List of authorization providers
|
|
418
|
+
- List of API tools
|
|
419
|
+
- List of custom types
|
|
18
420
|
|
|
19
421
|
Raises:
|
|
20
|
-
ImportError: If the OpenAPI spec cannot be loaded.
|
|
21
422
|
ValueError: If no valid endpoints are found in the spec.
|
|
22
423
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
424
|
+
|
|
425
|
+
# load the spec using
|
|
426
|
+
specification = parse(openapi_spec)
|
|
427
|
+
api_name = (
|
|
428
|
+
specification.info.title.lower().replace(" ", "-")
|
|
429
|
+
if specification.info and specification.info.title
|
|
430
|
+
else Path(openapi_spec).stem
|
|
431
|
+
)
|
|
432
|
+
# Keep only alphanumeric characters, hyphens, and underscores
|
|
433
|
+
api_name = "".join(c for c in api_name if c.isalnum() or c in "-_")
|
|
434
|
+
|
|
435
|
+
# If security is specified, create an authorization provider.
|
|
436
|
+
authorization_providers = [
|
|
437
|
+
to_authorization_provider(api_name, name.lower(), sec)
|
|
438
|
+
for name, sec in specification.security_schemas.items()
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
server_url = (
|
|
442
|
+
specification.servers[0].url
|
|
443
|
+
if specification.servers
|
|
444
|
+
else "http://localhost"
|
|
445
|
+
)
|
|
446
|
+
if not specification.servers:
|
|
447
|
+
logging.warning(
|
|
448
|
+
"No servers defined in the OpenAPI specification. Using http://localhost as default."
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Create tools from the parsed specification
|
|
452
|
+
existing_custom_types: dict[str, CustomType] = {}
|
|
453
|
+
tools = []
|
|
454
|
+
|
|
455
|
+
# Create a mapping from schema hash to their names in the OpenAPI spec
|
|
456
|
+
# Note: We can't monkey-patch here since the openapi_parser duplicates instances in memory
|
|
457
|
+
# if they are $ref'd in the content
|
|
458
|
+
schema_name_map: dict[int, str] = {
|
|
459
|
+
hash(str(schema)): name.replace(" ", "-").replace("_", "-")
|
|
460
|
+
for name, schema in specification.schemas.items()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# Get the default auth provider if available
|
|
464
|
+
default_auth = (
|
|
465
|
+
authorization_providers[0] if authorization_providers else None
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Iterate through all paths and operations
|
|
469
|
+
for path in specification.paths:
|
|
470
|
+
for operation in path.operations:
|
|
471
|
+
api_tool = to_api_tool(
|
|
472
|
+
server_url=server_url,
|
|
473
|
+
auth=default_auth,
|
|
474
|
+
path=path,
|
|
475
|
+
operation=operation,
|
|
476
|
+
existing_custom_types=existing_custom_types,
|
|
477
|
+
schema_name_map=schema_name_map,
|
|
478
|
+
)
|
|
479
|
+
tools.append(api_tool)
|
|
480
|
+
|
|
481
|
+
if not tools:
|
|
482
|
+
raise ValueError(
|
|
483
|
+
"No valid endpoints found in the OpenAPI specification"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Convert custom types to a list
|
|
487
|
+
custom_types = list(existing_custom_types.values())
|
|
488
|
+
|
|
489
|
+
return api_name, authorization_providers, tools, custom_types
|
|
@@ -8,8 +8,9 @@ from qtype.application.converters.types import PYTHON_TYPE_TO_PRIMITIVE_TYPE
|
|
|
8
8
|
from qtype.dsl.base_types import PrimitiveTypeEnum
|
|
9
9
|
from qtype.dsl.model import (
|
|
10
10
|
CustomType,
|
|
11
|
+
ListType,
|
|
11
12
|
PythonFunctionTool,
|
|
12
|
-
|
|
13
|
+
ToolParameter,
|
|
13
14
|
VariableType,
|
|
14
15
|
)
|
|
15
16
|
|
|
@@ -134,26 +135,23 @@ def _create_tool_from_function(
|
|
|
134
135
|
else f"Function {func_name}"
|
|
135
136
|
)
|
|
136
137
|
|
|
137
|
-
# Create input
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
id=func_name + "." + p["name"],
|
|
138
|
+
# Create input parameters from function parameters
|
|
139
|
+
inputs = {
|
|
140
|
+
p["name"]: ToolParameter(
|
|
141
141
|
type=_map_python_type_to_variable_type(p["type"], custom_types),
|
|
142
|
+
optional=p["default"] != inspect.Parameter.empty,
|
|
142
143
|
)
|
|
143
144
|
for p in func_info["parameters"]
|
|
144
|
-
|
|
145
|
+
}
|
|
145
146
|
|
|
146
|
-
# Create output
|
|
147
|
+
# Create output parameter based on return type
|
|
147
148
|
tool_id = func_info["module"] + "." + func_name
|
|
148
149
|
|
|
149
150
|
output_type = _map_python_type_to_variable_type(
|
|
150
151
|
func_info["return_type"], custom_types
|
|
151
152
|
)
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
id=f"{tool_id}.result",
|
|
155
|
-
type=output_type,
|
|
156
|
-
)
|
|
154
|
+
outputs = {"result": ToolParameter(type=output_type, optional=False)}
|
|
157
155
|
|
|
158
156
|
return PythonFunctionTool(
|
|
159
157
|
id=tool_id,
|
|
@@ -161,8 +159,8 @@ def _create_tool_from_function(
|
|
|
161
159
|
module_path=func_info["module"],
|
|
162
160
|
function_name=func_name,
|
|
163
161
|
description=description,
|
|
164
|
-
inputs=
|
|
165
|
-
outputs=
|
|
162
|
+
inputs=inputs if inputs else None,
|
|
163
|
+
outputs=outputs,
|
|
166
164
|
)
|
|
167
165
|
|
|
168
166
|
|
|
@@ -235,6 +233,32 @@ def _map_python_type_to_variable_type(
|
|
|
235
233
|
VariableType compatible value.
|
|
236
234
|
"""
|
|
237
235
|
|
|
236
|
+
# Check for generic types like list[str], list[int], etc.
|
|
237
|
+
origin = get_origin(python_type)
|
|
238
|
+
if origin is list:
|
|
239
|
+
# Handle list[T] annotations
|
|
240
|
+
args = get_args(python_type)
|
|
241
|
+
if len(args) == 1:
|
|
242
|
+
element_type_annotation = args[0]
|
|
243
|
+
# Recursively map the element type
|
|
244
|
+
element_type = _map_python_type_to_variable_type(
|
|
245
|
+
element_type_annotation, custom_types
|
|
246
|
+
)
|
|
247
|
+
# Support lists of both primitive types and custom types
|
|
248
|
+
if isinstance(element_type, PrimitiveTypeEnum):
|
|
249
|
+
return ListType(element_type=element_type)
|
|
250
|
+
elif isinstance(element_type, str):
|
|
251
|
+
# Custom type reference
|
|
252
|
+
return ListType(element_type=element_type)
|
|
253
|
+
else:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"List element type must be primitive or custom type, got: {element_type}"
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"List type must have exactly one type argument, got: {args}"
|
|
260
|
+
)
|
|
261
|
+
|
|
238
262
|
if python_type in PYTHON_TYPE_TO_PRIMITIVE_TYPE:
|
|
239
263
|
return PYTHON_TYPE_TO_PRIMITIVE_TYPE[python_type]
|
|
240
264
|
elif python_type in get_args(VariableType):
|
|
@@ -31,3 +31,17 @@ PYTHON_TYPE_TO_PRIMITIVE_TYPE = {
|
|
|
31
31
|
time: PrimitiveTypeEnum.time,
|
|
32
32
|
# TODO: decide on internal representation for images, video, and audio, or use annotation/hinting
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def python_type_for_list(element_type: PrimitiveTypeEnum) -> type:
|
|
37
|
+
"""
|
|
38
|
+
Get the Python list type for a given QType primitive element type.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
element_type: The primitive type of the list elements
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The corresponding Python list type (e.g., list[str] for text elements)
|
|
45
|
+
"""
|
|
46
|
+
element_python_type = PRIMITIVE_TO_PYTHON_TYPE[element_type]
|
|
47
|
+
return list[element_python_type]
|
qtype/application/facade.py
CHANGED
|
@@ -35,11 +35,15 @@ class QTypeFacade:
|
|
|
35
35
|
|
|
36
36
|
return load_document(Path(path).read_text(encoding="utf-8"))
|
|
37
37
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
def telemetry(self, spec: SemanticApplication) -> None:
|
|
39
|
+
if spec.telemetry:
|
|
40
|
+
logger.info(
|
|
41
|
+
f"Telemetry enabled with endpoint: {spec.telemetry.endpoint}"
|
|
42
|
+
)
|
|
43
|
+
# Register telemetry if needed
|
|
44
|
+
from qtype.interpreter.telemetry import register
|
|
45
|
+
|
|
46
|
+
register(spec.telemetry, spec.id)
|
|
43
47
|
|
|
44
48
|
def load_semantic_model(
|
|
45
49
|
self, path: PathLike
|
|
@@ -63,6 +67,7 @@ class QTypeFacade:
|
|
|
63
67
|
|
|
64
68
|
# Load the semantic application
|
|
65
69
|
semantic_model, type_registry = self.load_semantic_model(path)
|
|
70
|
+
self.telemetry(semantic_model)
|
|
66
71
|
|
|
67
72
|
# Find the flow to execute (inlined from _find_flow)
|
|
68
73
|
if flow_name:
|
|
@@ -95,6 +100,9 @@ class QTypeFacade:
|
|
|
95
100
|
else:
|
|
96
101
|
from qtype.interpreter.flow import execute_flow
|
|
97
102
|
|
|
103
|
+
for var in target_flow.inputs:
|
|
104
|
+
if var.id in inputs:
|
|
105
|
+
var.value = inputs[var.id]
|
|
98
106
|
args = {**kwargs, **inputs}
|
|
99
107
|
return execute_flow(target_flow, **args)
|
|
100
108
|
|
|
@@ -113,22 +121,11 @@ class QTypeFacade:
|
|
|
113
121
|
from qtype.dsl.model import Document
|
|
114
122
|
|
|
115
123
|
wrapped_document = Document(root=document)
|
|
124
|
+
from pydantic_yaml import to_yaml_str
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return to_yaml_str(
|
|
122
|
-
wrapped_document, exclude_unset=True, exclude_none=True
|
|
123
|
-
)
|
|
124
|
-
except ImportError:
|
|
125
|
-
# Fallback to basic YAML if pydantic_yaml is not available
|
|
126
|
-
import yaml
|
|
127
|
-
|
|
128
|
-
document_dict = wrapped_document.model_dump(
|
|
129
|
-
exclude_unset=True, exclude_none=True
|
|
130
|
-
)
|
|
131
|
-
return yaml.dump(document_dict, default_flow_style=False)
|
|
126
|
+
return to_yaml_str(
|
|
127
|
+
wrapped_document, exclude_unset=True, exclude_none=True
|
|
128
|
+
)
|
|
132
129
|
|
|
133
130
|
def generate_aws_bedrock_models(self) -> list[dict[str, Any]]:
|
|
134
131
|
"""
|