qtype 0.0.11__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.
Files changed (49) hide show
  1. qtype/application/converters/tools_from_api.py +476 -11
  2. qtype/application/converters/tools_from_module.py +37 -13
  3. qtype/application/converters/types.py +17 -3
  4. qtype/application/facade.py +17 -20
  5. qtype/commands/convert.py +36 -2
  6. qtype/commands/generate.py +48 -0
  7. qtype/commands/run.py +1 -0
  8. qtype/commands/serve.py +11 -1
  9. qtype/commands/validate.py +8 -11
  10. qtype/commands/visualize.py +0 -3
  11. qtype/dsl/model.py +190 -4
  12. qtype/dsl/validator.py +2 -1
  13. qtype/interpreter/api.py +5 -1
  14. qtype/interpreter/batch/file_sink_source.py +162 -0
  15. qtype/interpreter/batch/flow.py +1 -1
  16. qtype/interpreter/batch/sql_source.py +3 -6
  17. qtype/interpreter/batch/step.py +12 -1
  18. qtype/interpreter/batch/utils.py +8 -9
  19. qtype/interpreter/step.py +2 -2
  20. qtype/interpreter/steps/tool.py +194 -28
  21. qtype/interpreter/ui/404/index.html +1 -1
  22. qtype/interpreter/ui/404.html +1 -1
  23. qtype/interpreter/ui/_next/static/chunks/393-8fd474427f8e19ce.js +36 -0
  24. qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
  25. qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
  26. qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +1 -0
  27. qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
  28. qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
  29. qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +3 -0
  30. qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  31. qtype/interpreter/ui/index.html +1 -1
  32. qtype/interpreter/ui/index.txt +4 -4
  33. qtype/loader.py +8 -2
  34. qtype/semantic/generate.py +6 -2
  35. qtype/semantic/model.py +132 -77
  36. qtype/semantic/visualize.py +24 -6
  37. {qtype-0.0.11.dist-info → qtype-0.0.13.dist-info}/METADATA +4 -2
  38. {qtype-0.0.11.dist-info → qtype-0.0.13.dist-info}/RECORD +44 -43
  39. qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
  40. qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
  41. qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
  42. qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
  43. qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
  44. /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → nUaw6_IwRwPqkzwe5s725}/_buildManifest.js +0 -0
  45. /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → nUaw6_IwRwPqkzwe5s725}/_ssgManifest.js +0 -0
  46. {qtype-0.0.11.dist-info → qtype-0.0.13.dist-info}/WHEEL +0 -0
  47. {qtype-0.0.11.dist-info → qtype-0.0.13.dist-info}/entry_points.txt +0 -0
  48. {qtype-0.0.11.dist-info → qtype-0.0.13.dist-info}/licenses/LICENSE +0 -0
  49. {qtype-0.0.11.dist-info → qtype-0.0.13.dist-info}/top_level.txt +0 -0
@@ -1,24 +1,489 @@
1
- from qtype.dsl.model import APITool, AuthorizationProvider
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
- exclude_paths: list[str] | None = None,
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
- Load tools from an OpenAPI specification by introspecting its endpoints.
409
+ Creates tools from an OpenAPI specification.
12
410
 
13
411
  Args:
14
- module_path: The OpenAPI specification path or URL.
412
+ openapi_spec: The OpenAPI specification path or URL.
15
413
 
16
414
  Returns:
17
- List of OpenAPIToolProvider instances created from the OpenAPI spec.
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
- # Placeholder for actual implementation
24
- raise NotImplementedError("OpenAPI tool loading not yet implemented")
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
- Variable,
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 variables from function parameters
138
- input_variables = [
139
- Variable(
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 variable based on return type
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
- output_variable = Variable(
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=input_variables if len(input_variables) > 0 else None, # type: ignore
165
- outputs=[output_variable],
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):
@@ -9,14 +9,14 @@ PRIMITIVE_TO_PYTHON_TYPE = {
9
9
  PrimitiveTypeEnum.audio: bytes,
10
10
  PrimitiveTypeEnum.boolean: bool,
11
11
  PrimitiveTypeEnum.bytes: bytes,
12
- PrimitiveTypeEnum.date: str, # Use str for date representation
13
- PrimitiveTypeEnum.datetime: str, # Use str for datetime representation
12
+ PrimitiveTypeEnum.date: date,
13
+ PrimitiveTypeEnum.datetime: datetime,
14
14
  PrimitiveTypeEnum.int: int,
15
15
  PrimitiveTypeEnum.file: bytes, # Use bytes for file content
16
16
  PrimitiveTypeEnum.float: float,
17
17
  PrimitiveTypeEnum.image: bytes, # Use bytes for image data
18
18
  PrimitiveTypeEnum.text: str,
19
- PrimitiveTypeEnum.time: str, # Use str for time representation
19
+ PrimitiveTypeEnum.time: time, # Use time for time representation
20
20
  PrimitiveTypeEnum.video: bytes, # Use bytes for video data
21
21
  }
22
22
 
@@ -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]
@@ -35,11 +35,15 @@ class QTypeFacade:
35
35
 
36
36
  return load_document(Path(path).read_text(encoding="utf-8"))
37
37
 
38
- def load_and_validate(self, path: PathLike) -> DocumentRootType:
39
- """Load and validate a document."""
40
- logger.info("Document loaded, proceeding to validation")
41
- root, _ = self.load_dsl_document(path)
42
- return root
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
- # Try to use pydantic_yaml first
118
- try:
119
- from pydantic_yaml import to_yaml_str
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
  """