qtype 0.0.12__py3-none-any.whl → 0.1.7__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/commons/tools.py +1 -1
- qtype/application/converters/tools_from_api.py +476 -11
- qtype/application/converters/tools_from_module.py +38 -14
- qtype/application/converters/types.py +15 -30
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +102 -85
- qtype/base/types.py +227 -7
- qtype/cli.py +5 -1
- qtype/commands/convert.py +52 -6
- qtype/commands/generate.py +44 -4
- qtype/commands/run.py +78 -36
- qtype/commands/serve.py +74 -44
- qtype/commands/validate.py +37 -14
- qtype/commands/visualize.py +46 -25
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +86 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +753 -264
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +63 -136
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +436 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +91 -0
- qtype/interpreter/base/factory.py +84 -0
- qtype/interpreter/base/progress_tracker.py +110 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +74 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +495 -24
- qtype/interpreter/converters.py +79 -0
- qtype/interpreter/endpoints.py +355 -0
- qtype/interpreter/executors/agent_executor.py +242 -0
- qtype/interpreter/executors/aggregate_executor.py +93 -0
- qtype/interpreter/executors/bedrock_reranker_executor.py +195 -0
- qtype/interpreter/executors/decoder_executor.py +163 -0
- qtype/interpreter/executors/doc_to_text_executor.py +112 -0
- qtype/interpreter/executors/document_embedder_executor.py +123 -0
- qtype/interpreter/executors/document_search_executor.py +113 -0
- qtype/interpreter/executors/document_source_executor.py +118 -0
- qtype/interpreter/executors/document_splitter_executor.py +105 -0
- qtype/interpreter/executors/echo_executor.py +63 -0
- qtype/interpreter/executors/field_extractor_executor.py +165 -0
- qtype/interpreter/executors/file_source_executor.py +101 -0
- qtype/interpreter/executors/file_writer_executor.py +110 -0
- qtype/interpreter/executors/index_upsert_executor.py +232 -0
- qtype/interpreter/executors/invoke_embedding_executor.py +104 -0
- qtype/interpreter/executors/invoke_flow_executor.py +51 -0
- qtype/interpreter/executors/invoke_tool_executor.py +358 -0
- qtype/interpreter/executors/llm_inference_executor.py +272 -0
- qtype/interpreter/executors/prompt_template_executor.py +78 -0
- qtype/interpreter/executors/sql_source_executor.py +106 -0
- qtype/interpreter/executors/vector_search_executor.py +91 -0
- qtype/interpreter/flow.py +172 -22
- qtype/interpreter/logging_progress.py +61 -0
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- qtype/interpreter/rich_progress.py +225 -0
- qtype/interpreter/stream/chat/__init__.py +15 -0
- qtype/interpreter/stream/chat/converter.py +391 -0
- qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
- qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
- qtype/interpreter/stream/chat/vercel.py +609 -0
- qtype/interpreter/stream/utils/__init__.py +15 -0
- qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
- qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
- qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
- qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
- qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
- qtype/interpreter/telemetry.py +135 -8
- qtype/interpreter/tools/__init__.py +5 -0
- qtype/interpreter/tools/function_tool_helper.py +265 -0
- qtype/interpreter/types.py +330 -0
- qtype/interpreter/typing.py +83 -89
- qtype/interpreter/ui/404/index.html +1 -1
- qtype/interpreter/ui/404.html +1 -1
- qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.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-8c67d16ac90d23cb.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.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/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +5 -5
- qtype/semantic/checker.py +643 -0
- qtype/semantic/generate.py +268 -85
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +535 -163
- qtype/semantic/resolver.py +63 -19
- qtype/semantic/visualize.py +50 -35
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/METADATA +22 -5
- qtype-0.1.7.dist-info/RECORD +137 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -464
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -95
- qtype/interpreter/batch/step.py +0 -63
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -179
- qtype/interpreter/chat/chat_api.py +0 -237
- qtype/interpreter/chat/vercel.py +0 -314
- qtype/interpreter/exceptions.py +0 -10
- qtype/interpreter/step.py +0 -67
- qtype/interpreter/steps/__init__.py +0 -0
- qtype/interpreter/steps/agent.py +0 -114
- qtype/interpreter/steps/condition.py +0 -36
- qtype/interpreter/steps/decoder.py +0 -88
- qtype/interpreter/steps/llm_inference.py +0 -150
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -53
- qtype/interpreter/streaming_helpers.py +0 -123
- 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/ba12c10f-22556063851a6df2.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/favicon.ico +0 -0
- qtype/loader.py +0 -389
- qtype-0.0.12.dist-info/RECORD +0 -105
- /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/WHEEL +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -180,7 +180,7 @@ def parse_duration_string(duration: str) -> int:
|
|
|
180
180
|
|
|
181
181
|
def format_datetime(timestamp: datetime, format_string: str) -> str:
|
|
182
182
|
"""
|
|
183
|
-
Format a timestamp using a custom format string.
|
|
183
|
+
Format a timestamp using a custom format string that can be passed to strftime.
|
|
184
184
|
|
|
185
185
|
Args:
|
|
186
186
|
timestamp: Datetime object to format.
|
|
@@ -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.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,
|
|
348
|
+
outputs=outputs,
|
|
349
|
+
parameters=parameters,
|
|
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 [],
|
|
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
|
|
@@ -5,11 +5,12 @@ from typing import Any, Type, Union, get_args, get_origin
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
7
|
from qtype.application.converters.types import PYTHON_TYPE_TO_PRIMITIVE_TYPE
|
|
8
|
-
from qtype.
|
|
8
|
+
from qtype.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,
|
|
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):
|
|
@@ -1,33 +1,18 @@
|
|
|
1
|
-
from datetime import date, datetime, time
|
|
2
|
-
|
|
3
|
-
from qtype.dsl.base_types import PrimitiveTypeEnum
|
|
4
|
-
|
|
5
1
|
"""
|
|
6
|
-
|
|
2
|
+
Re-export type mappings from DSL layer for backward compatibility.
|
|
3
|
+
|
|
4
|
+
This module maintains the application layer's interface while delegating
|
|
5
|
+
to the DSL layer where these mappings are now defined.
|
|
7
6
|
"""
|
|
8
|
-
PRIMITIVE_TO_PYTHON_TYPE = {
|
|
9
|
-
PrimitiveTypeEnum.audio: bytes,
|
|
10
|
-
PrimitiveTypeEnum.boolean: bool,
|
|
11
|
-
PrimitiveTypeEnum.bytes: bytes,
|
|
12
|
-
PrimitiveTypeEnum.date: date,
|
|
13
|
-
PrimitiveTypeEnum.datetime: datetime,
|
|
14
|
-
PrimitiveTypeEnum.int: int,
|
|
15
|
-
PrimitiveTypeEnum.file: bytes, # Use bytes for file content
|
|
16
|
-
PrimitiveTypeEnum.float: float,
|
|
17
|
-
PrimitiveTypeEnum.image: bytes, # Use bytes for image data
|
|
18
|
-
PrimitiveTypeEnum.text: str,
|
|
19
|
-
PrimitiveTypeEnum.time: time, # Use time for time representation
|
|
20
|
-
PrimitiveTypeEnum.video: bytes, # Use bytes for video data
|
|
21
|
-
}
|
|
22
7
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
8
|
+
from qtype.dsl.types import (
|
|
9
|
+
PRIMITIVE_TO_PYTHON_TYPE,
|
|
10
|
+
PYTHON_TYPE_TO_PRIMITIVE_TYPE,
|
|
11
|
+
python_type_for_list,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PRIMITIVE_TO_PYTHON_TYPE",
|
|
16
|
+
"PYTHON_TYPE_TO_PRIMITIVE_TYPE",
|
|
17
|
+
"python_type_for_list",
|
|
18
|
+
]
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Type, Union, get_args, get_origin
|
|
6
6
|
|
|
7
7
|
import qtype.dsl.model as dsl
|
|
8
|
-
from qtype.
|
|
8
|
+
from qtype.base.types import PrimitiveTypeEnum
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _format_type_name(field_type: Any) -> str:
|