qtype 0.0.1__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/__init__.py +0 -0
  2. qtype/cli.py +73 -0
  3. qtype/commands/__init__.py +5 -0
  4. qtype/commands/convert.py +76 -0
  5. qtype/commands/generate.py +107 -0
  6. qtype/commands/run.py +200 -0
  7. qtype/commands/validate.py +83 -0
  8. qtype/commons/__init__.py +0 -0
  9. qtype/commons/generate.py +88 -0
  10. qtype/commons/tools.py +192 -0
  11. qtype/converters/__init__.py +0 -0
  12. qtype/converters/tools_from_api.py +24 -0
  13. qtype/converters/tools_from_module.py +326 -0
  14. qtype/converters/types.py +20 -0
  15. qtype/dsl/__init__.py +1 -0
  16. qtype/dsl/base_types.py +31 -0
  17. qtype/dsl/document.py +108 -0
  18. qtype/dsl/domain_types.py +56 -0
  19. qtype/dsl/model.py +685 -0
  20. qtype/dsl/validator.py +439 -0
  21. qtype/interpreter/__init__.py +1 -0
  22. qtype/interpreter/api.py +104 -0
  23. qtype/interpreter/conversions.py +148 -0
  24. qtype/interpreter/exceptions.py +10 -0
  25. qtype/interpreter/flow.py +37 -0
  26. qtype/interpreter/resource_cache.py +37 -0
  27. qtype/interpreter/step.py +67 -0
  28. qtype/interpreter/steps/__init__.py +0 -0
  29. qtype/interpreter/steps/agent.py +114 -0
  30. qtype/interpreter/steps/condition.py +36 -0
  31. qtype/interpreter/steps/decoder.py +84 -0
  32. qtype/interpreter/steps/llm_inference.py +127 -0
  33. qtype/interpreter/steps/prompt_template.py +54 -0
  34. qtype/interpreter/steps/search.py +24 -0
  35. qtype/interpreter/steps/tool.py +53 -0
  36. qtype/interpreter/telemetry.py +16 -0
  37. qtype/interpreter/typing.py +78 -0
  38. qtype/loader.py +341 -0
  39. qtype/semantic/__init__.py +0 -0
  40. qtype/semantic/errors.py +4 -0
  41. qtype/semantic/generate.py +383 -0
  42. qtype/semantic/model.py +354 -0
  43. qtype/semantic/resolver.py +97 -0
  44. qtype-0.0.1.dist-info/METADATA +120 -0
  45. qtype-0.0.1.dist-info/RECORD +49 -0
  46. qtype-0.0.1.dist-info/WHEEL +5 -0
  47. qtype-0.0.1.dist-info/entry_points.txt +2 -0
  48. qtype-0.0.1.dist-info/licenses/LICENSE +202 -0
  49. qtype-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Type
4
+
5
+ from pydantic import BaseModel, Field, create_model
6
+
7
+ from qtype.converters.types import PRIMITIVE_TO_PYTHON_TYPE
8
+ from qtype.dsl.model import DOMAIN_CLASSES, PrimitiveTypeEnum
9
+ from qtype.semantic.model import Flow, Variable
10
+
11
+
12
+ def _get_variable_type(var: Variable) -> Type:
13
+ if isinstance(var.type, PrimitiveTypeEnum):
14
+ return PRIMITIVE_TO_PYTHON_TYPE.get(var.type, str)
15
+ elif var.type.__name__ in DOMAIN_CLASSES:
16
+ return DOMAIN_CLASSES[var.type.__name__]
17
+ else:
18
+ # TODO: handle custom TypeDefinition...
19
+ raise ValueError(f"Unsupported variable type: {var.type}")
20
+
21
+
22
+ def create_output_type_model(flow: Flow) -> Type[BaseModel]:
23
+ """Dynamically create a Pydantic response model for a flow."""
24
+ fields = {}
25
+
26
+ # Always include flow_id and status
27
+ fields["flow_id"] = (str, Field(description="ID of the executed flow"))
28
+ fields["status"] = (str, Field(description="Execution status"))
29
+
30
+ # Add dynamic output fields
31
+ if flow.outputs:
32
+ output_fields = {}
33
+ for var in flow.outputs:
34
+ python_type = _get_variable_type(var)
35
+ field_info = Field(
36
+ description=f"Output for {var.id}",
37
+ title=var.id,
38
+ )
39
+ output_fields[var.id] = (python_type, field_info)
40
+
41
+ # Create nested outputs model
42
+ outputs_model = create_model(
43
+ f"{flow.id}Outputs",
44
+ __base__=BaseModel,
45
+ **output_fields,
46
+ ) # type: ignore
47
+ fields["outputs"] = (
48
+ outputs_model,
49
+ Field(description="Flow execution outputs"),
50
+ )
51
+ else:
52
+ fields["outputs"] = (
53
+ dict[str, Any],
54
+ Field(description="Flow execution outputs"),
55
+ ) # type: ignore
56
+
57
+ return create_model(f"{flow.id}Response", __base__=BaseModel, **fields) # type: ignore
58
+
59
+
60
+ def create_input_type_model(flow: Flow) -> Type[BaseModel]:
61
+ """Dynamically create a Pydantic request model for a flow."""
62
+ if not flow.inputs:
63
+ # Return a simple model with no required fields
64
+ return create_model(
65
+ f"{flow.id}Request",
66
+ __base__=BaseModel,
67
+ )
68
+
69
+ fields = {}
70
+ for var in flow.inputs:
71
+ python_type = _get_variable_type(var) # type: ignore
72
+ field_info = Field(
73
+ description=f"Input for {var.id}",
74
+ title=var.id,
75
+ )
76
+ fields[var.id] = (python_type, field_info)
77
+
78
+ return create_model(f"{flow.id}Request", __base__=BaseModel, **fields) # type: ignore
qtype/loader.py ADDED
@@ -0,0 +1,341 @@
1
+ """
2
+ YAML loading and validation with environment variable support and file inclusion.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.parse import urljoin, urlparse
12
+
13
+ import fsspec
14
+ import yaml
15
+ from dotenv import load_dotenv
16
+ from fsspec.core import url_to_fs
17
+
18
+ from qtype.dsl import model as dsl
19
+ from qtype.dsl.validator import validate
20
+ from qtype.semantic.model import Application
21
+ from qtype.semantic.resolver import resolve
22
+
23
+
24
+ class _StringStream:
25
+ """
26
+ A file-like stream wrapper around string content for YAML loading.
27
+ This class provides a readable stream interface that PyYAML can use
28
+ to parse string content as if it were reading from a file.
29
+ """
30
+
31
+ def __init__(self, content: str, name: str | None = None) -> None:
32
+ """
33
+ Initialize the string stream.
34
+
35
+ Args:
36
+ content: The string content to wrap.
37
+ name: Optional name/path for the stream (used for relative path resolution).
38
+ """
39
+ self.content = content
40
+ self.name = name
41
+ self._pos = 0
42
+
43
+ def read(self, size: int = -1) -> str:
44
+ """
45
+ Read content from the stream.
46
+
47
+ Args:
48
+ size: Number of characters to read. If -1, read all remaining content.
49
+
50
+ Returns:
51
+ The requested content as a string.
52
+ """
53
+ if size == -1:
54
+ result = self.content[self._pos :]
55
+ self._pos = len(self.content)
56
+ else:
57
+ result = self.content[self._pos : self._pos + size]
58
+ self._pos += len(result)
59
+ return result
60
+
61
+
62
+ class YamlLoader(yaml.SafeLoader):
63
+ """
64
+ YAML loader that supports environment variable substitution and file inclusion.
65
+
66
+ Supports the following syntax:
67
+ - ${VAR_NAME} - Required environment variable (raises error if not found)
68
+ - ${VAR_NAME:default_value} - Optional with default value
69
+ - !include path/to/file.yaml - Include external YAML file
70
+ - !include_raw path/to/file.txt - Include raw text file as string
71
+
72
+ File paths can be:
73
+ - Local filesystem paths (relative or absolute)
74
+ - URLs (http://, https://)
75
+ - GitHub URLs (github://)
76
+ - S3 URLs (s3://)
77
+ - Any fsspec-supported protocol
78
+ """
79
+
80
+ def __init__(self, stream: Any) -> None:
81
+ super().__init__(stream)
82
+ # Store the base path/URL of the current file for relative path resolution
83
+ if hasattr(stream, "name") and stream.name is not None:
84
+ self._current_path = stream.name
85
+ else:
86
+ self._current_path = str(Path.cwd())
87
+
88
+
89
+ def _env_var_constructor(loader: YamlLoader, node: yaml.ScalarNode) -> str:
90
+ """
91
+ Constructor for environment variable substitution.
92
+
93
+ Args:
94
+ loader: The YAML loader instance.
95
+ node: The YAML node containing the environment variable reference.
96
+
97
+ Returns:
98
+ The resolved environment variable value.
99
+
100
+ Raises:
101
+ ValueError: If a required environment variable is not found.
102
+ """
103
+ value = loader.construct_scalar(node)
104
+
105
+ # Pattern to match ${VAR_NAME} or ${VAR_NAME:default}
106
+ pattern = r"\$\{([^}:]+)(?::([^}]*))?\}"
107
+
108
+ def replace_env_var(match: re.Match[str]) -> str:
109
+ var_name = match.group(1)
110
+ default_value = match.group(2)
111
+
112
+ env_value = os.getenv(var_name)
113
+
114
+ if env_value is not None:
115
+ return env_value
116
+ elif default_value is not None:
117
+ return default_value
118
+ else:
119
+ msg = f"Environment variable '{var_name}' is required but not set"
120
+ raise ValueError(msg)
121
+
122
+ return re.sub(pattern, replace_env_var, value)
123
+
124
+
125
+ def _include_file_constructor(
126
+ loader: YamlLoader, node: yaml.ScalarNode
127
+ ) -> Any:
128
+ """
129
+ Constructor for !include tag to load external YAML files using fsspec.
130
+
131
+ Args:
132
+ loader: The YAML loader instance.
133
+ node: The YAML node containing the file path/URL.
134
+
135
+ Returns:
136
+ The parsed YAML data from the included file.
137
+
138
+ Raises:
139
+ FileNotFoundError: If the included file doesn't exist.
140
+ yaml.YAMLError: If the included file is malformed YAML.
141
+ """
142
+ file_path = loader.construct_scalar(node)
143
+
144
+ # Resolve relative paths/URLs relative to the current file
145
+ resolved_path = _resolve_path(loader._current_path, file_path)
146
+
147
+ try:
148
+ with fsspec.open(resolved_path, "r", encoding="utf-8") as f:
149
+ content = f.read() # type: ignore[misc]
150
+
151
+ # Create a string stream with the resolved path for nested includes
152
+ stream = _StringStream(content, resolved_path)
153
+ return yaml.load(stream, Loader=YamlLoader)
154
+ except ValueError:
155
+ # Re-raise ValueError (e.g., missing environment variables) without wrapping
156
+ raise
157
+ except Exception as e:
158
+ msg = f"Failed to load included file '{resolved_path}': {e}"
159
+ raise FileNotFoundError(msg) from e
160
+
161
+
162
+ def _include_raw_constructor(loader: YamlLoader, node: yaml.ScalarNode) -> str:
163
+ """
164
+ Constructor for !include_raw tag to load external text files using fsspec.
165
+
166
+ Args:
167
+ loader: The YAML loader instance.
168
+ node: The YAML node containing the file path/URL.
169
+
170
+ Returns:
171
+ The raw text content of the included file.
172
+
173
+ Raises:
174
+ FileNotFoundError: If the included file doesn't exist.
175
+ """
176
+ file_path = loader.construct_scalar(node)
177
+
178
+ # Resolve relative paths/URLs relative to the current file
179
+ resolved_path = _resolve_path(loader._current_path, file_path)
180
+
181
+ try:
182
+ with fsspec.open(resolved_path, "r", encoding="utf-8") as f:
183
+ return f.read() # type: ignore[misc]
184
+ except Exception as e:
185
+ msg = f"Failed to load included file '{resolved_path}': {e}"
186
+ raise FileNotFoundError(msg) from e
187
+
188
+
189
+ def _resolve_path(current_path: str, target_path: str) -> str:
190
+ """
191
+ Resolve a target path relative to the current file path.
192
+
193
+ Args:
194
+ current_path: The path/URL of the current file.
195
+ target_path: The target path/URL to resolve.
196
+
197
+ Returns:
198
+ The resolved absolute path/URL.
199
+ """
200
+ # If target is already absolute (has scheme or starts with /), use as-is
201
+ parsed_target = urlparse(target_path)
202
+ if parsed_target.scheme or target_path.startswith("/"):
203
+ return target_path
204
+
205
+ # Check if current path is a URL
206
+ parsed_current = urlparse(current_path)
207
+ if parsed_current.scheme:
208
+ # Current is a URL, use urljoin for proper URL resolution
209
+ return urljoin(current_path, target_path)
210
+ else:
211
+ # Current is a local path, resolve relative to its directory
212
+ current_dir = Path(current_path).parent
213
+ return str(current_dir / target_path)
214
+
215
+
216
+ def _load_env_files(directories: list[Path]) -> None:
217
+ """Load .env files from the specified directories."""
218
+ for directory in directories:
219
+ env_file = directory / ".env"
220
+ if env_file.exists():
221
+ load_dotenv(env_file)
222
+
223
+
224
+ # Register constructors for YamlLoader
225
+ YamlLoader.add_constructor("tag:yaml.org,2002:str", _env_var_constructor)
226
+ YamlLoader.add_constructor("!include", _include_file_constructor)
227
+ YamlLoader.add_constructor("!include_raw", _include_raw_constructor)
228
+
229
+
230
+ def load_yaml_from_string(
231
+ content: str, original_uri: str | None = None
232
+ ) -> dict[str, Any]:
233
+ """
234
+ Load a YAML file with environment variable substitution and file inclusion support.
235
+
236
+ Args:
237
+ content: The YAML content to load.
238
+
239
+ Returns:
240
+ The parsed YAML data with includes resolved and environment variables substituted.
241
+
242
+ Raises:
243
+ ValueError: If a required environment variable is not found.
244
+ FileNotFoundError: If the YAML file or included files don't exist.
245
+ yaml.YAMLError: If the YAML file is malformed.
246
+ """
247
+
248
+ # Create a string stream for the loader
249
+ # Note: When loading from string, relative paths will be resolved relative to cwd
250
+ stream = _StringStream(content, original_uri)
251
+ # Use the string stream directly with the loader
252
+ result = yaml.load(stream, Loader=YamlLoader)
253
+
254
+ return result
255
+
256
+
257
+ def load_yaml(content: str) -> dict[str, Any]:
258
+ """
259
+ Load a YAML file with environment variable substitution and file inclusion support.
260
+
261
+ Args:
262
+ content: Either a fsspec uri/file path to load, or a string containing YAML content.
263
+
264
+ Returns:
265
+ The parsed YAML data with includes resolved and environment variables substituted.
266
+
267
+ Raises:
268
+ ValueError: If a required environment variable is not found.
269
+ FileNotFoundError: If the YAML file or included files don't exist.
270
+ yaml.YAMLError: If the YAML file is malformed.
271
+ """
272
+ try:
273
+ # First check if content looks like a file path or URI
274
+ if "\n" in content:
275
+ # If it contains newlines, treat as raw YAML content
276
+ is_uri = False
277
+ else:
278
+ # it has no new lines, so it's probably a uri
279
+ # try to resolve it
280
+ _ = url_to_fs(content)
281
+ is_uri = True
282
+ except (ValueError, OSError):
283
+ is_uri = False
284
+
285
+ # Load the environment variables from .env files
286
+ directories = [Path.cwd()]
287
+
288
+ if is_uri:
289
+ # if the content is a uri, see if it is a local path. if it is, add the directory
290
+ try:
291
+ parsed = urlparse(content)
292
+ if parsed.scheme in ["file", ""]:
293
+ # For file-like URIs, resolve the path and add its directory
294
+ directories.append(Path(parsed.path).parent)
295
+ except Exception:
296
+ pass
297
+
298
+ # Load .env files from the specified directories
299
+ _load_env_files(directories)
300
+
301
+ # Load the yaml content
302
+ if is_uri:
303
+ original_uri = content
304
+ with fsspec.open(content, "r", encoding="utf-8") as f:
305
+ content = f.read() # type: ignore[misc]
306
+ return load_yaml_from_string(content, original_uri)
307
+ else:
308
+ return load_yaml_from_string(content)
309
+
310
+
311
+ ResolveableType = dsl.Agent | dsl.Application | dsl.Flow | list
312
+
313
+
314
+ def _resolve_root(doc: dsl.Document) -> ResolveableType:
315
+ root = doc.root
316
+ # If the docroot is a type that ends in the name `List`, resolve it again
317
+ types_to_resolve = set(
318
+ [
319
+ dsl.AuthorizationProviderList,
320
+ dsl.IndexList,
321
+ dsl.ModelList,
322
+ dsl.ToolList,
323
+ dsl.VariableList,
324
+ ]
325
+ )
326
+ if root is not None and type(root) in types_to_resolve:
327
+ root = root.root # type: ignore
328
+ return root # type: ignore[return-value]
329
+
330
+
331
+ def load(content: str) -> Application:
332
+ """Load a QType YAML file, validate it, and return the resolved root."""
333
+ yaml_data = load_yaml(content)
334
+ document = dsl.Document.model_validate(yaml_data)
335
+ document = _resolve_root(document)
336
+ if not isinstance(document, dsl.Application):
337
+ raise ValueError(
338
+ f"Root document is not an Application, found {type(document)}."
339
+ )
340
+ document = validate(document)
341
+ return resolve(document)
File without changes
@@ -0,0 +1,4 @@
1
+ class SemanticResolutionError(Exception):
2
+ """Raised when there's an error during semantic resolution."""
3
+
4
+ pass