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
qtype/application/facade.py
CHANGED
|
@@ -2,69 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel
|
|
10
|
-
|
|
11
|
-
from qtype.base.logging import get_logger
|
|
12
|
-
from qtype.base.types import CustomTypeRegistry, DocumentRootType, PathLike
|
|
13
|
-
from qtype.dsl.base_types import StepCardinality
|
|
14
|
-
from qtype.dsl.model import Application as DSLApplication
|
|
15
|
-
from qtype.dsl.model import DocumentType
|
|
16
|
-
from qtype.interpreter.batch.types import BatchConfig
|
|
9
|
+
from qtype.base.types import PathLike
|
|
17
10
|
from qtype.semantic.model import Application as SemanticApplication
|
|
18
|
-
from qtype.semantic.model import
|
|
11
|
+
from qtype.semantic.model import DocumentType as SemanticDocumentType
|
|
12
|
+
|
|
13
|
+
# Note: There should be _zero_ imports here at the top that import qtype.interpreter.
|
|
14
|
+
# That's the whole point of this facade - to avoid importing optional
|
|
15
|
+
# dependencies unless these methods are called.
|
|
19
16
|
|
|
20
|
-
logger =
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
class QTypeFacade:
|
|
24
21
|
"""
|
|
25
|
-
Simplified interface for
|
|
22
|
+
Simplified interface for qtype operations.
|
|
26
23
|
|
|
27
|
-
This facade
|
|
28
|
-
|
|
24
|
+
This facade provides lazy-loading wrappers for operations that require
|
|
25
|
+
optional dependencies (interpreter package), allowing base qtype to work
|
|
26
|
+
without those dependencies installed.
|
|
29
27
|
"""
|
|
30
28
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
) -> tuple[DocumentRootType, CustomTypeRegistry]:
|
|
34
|
-
from qtype.loader import load_document
|
|
29
|
+
def telemetry(self, spec: SemanticDocumentType) -> None:
|
|
30
|
+
from qtype.interpreter.telemetry import register
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
if isinstance(spec, SemanticApplication) and spec.telemetry:
|
|
33
|
+
logger.info(
|
|
34
|
+
f"Telemetry enabled with endpoint: {spec.telemetry.endpoint}"
|
|
35
|
+
)
|
|
36
|
+
# Register telemetry if needed
|
|
37
|
+
register(spec.telemetry, self.secret_manager(spec), spec.id)
|
|
37
38
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
root, _ = self.load_dsl_document(path)
|
|
42
|
-
return root
|
|
39
|
+
def secret_manager(self, spec: SemanticDocumentType):
|
|
40
|
+
"""
|
|
41
|
+
Create a secret manager based on the specification.
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
) -> tuple[SemanticApplication, CustomTypeRegistry]:
|
|
47
|
-
"""Load a document and return the resolved semantic model."""
|
|
48
|
-
from qtype.loader import load
|
|
43
|
+
Args:
|
|
44
|
+
spec: SemanticDocumentType specification
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
Returns:
|
|
47
|
+
Secret manager instance
|
|
48
|
+
"""
|
|
49
|
+
from qtype.interpreter.base.secrets import create_secret_manager
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
if isinstance(spec, SemanticApplication):
|
|
52
|
+
return create_secret_manager(spec.secret_manager)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"Can't create secret manager for non-Application spec"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def execute_workflow(
|
|
54
59
|
self,
|
|
55
60
|
path: PathLike,
|
|
56
|
-
inputs: dict |
|
|
61
|
+
inputs: dict | Any,
|
|
57
62
|
flow_name: str | None = None,
|
|
58
|
-
batch_config: BatchConfig | None = None,
|
|
59
63
|
**kwargs: Any,
|
|
60
|
-
) ->
|
|
61
|
-
"""
|
|
62
|
-
|
|
64
|
+
) -> Any:
|
|
65
|
+
"""
|
|
66
|
+
Execute a complete workflow from document to results.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
path: Path to the QType specification file
|
|
70
|
+
inputs: Dictionary of input values or DataFrame for batch
|
|
71
|
+
flow_name: Optional name of flow to execute
|
|
72
|
+
**kwargs: Additional dependencies for execution
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
DataFrame with results (one row per input)
|
|
76
|
+
"""
|
|
77
|
+
import pandas as pd
|
|
78
|
+
from opentelemetry import trace
|
|
79
|
+
|
|
80
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
81
|
+
from qtype.interpreter.converters import (
|
|
82
|
+
dataframe_to_flow_messages,
|
|
83
|
+
flow_messages_to_dataframe,
|
|
84
|
+
)
|
|
85
|
+
from qtype.interpreter.flow import run_flow
|
|
86
|
+
from qtype.interpreter.types import Session
|
|
87
|
+
from qtype.semantic.loader import load
|
|
63
88
|
|
|
64
89
|
# Load the semantic application
|
|
65
|
-
semantic_model, type_registry =
|
|
90
|
+
semantic_model, type_registry = load(Path(path))
|
|
91
|
+
assert isinstance(semantic_model, SemanticApplication)
|
|
92
|
+
self.telemetry(semantic_model)
|
|
66
93
|
|
|
67
|
-
# Find the flow to execute
|
|
94
|
+
# Find the flow to execute
|
|
68
95
|
if flow_name:
|
|
69
96
|
target_flow = None
|
|
70
97
|
for flow in semantic_model.flows:
|
|
@@ -78,57 +105,47 @@ class QTypeFacade:
|
|
|
78
105
|
target_flow = semantic_model.flows[0]
|
|
79
106
|
else:
|
|
80
107
|
raise ValueError("No flows found in application")
|
|
81
|
-
if target_flow.cardinality == StepCardinality.many:
|
|
82
|
-
if isinstance(inputs, dict):
|
|
83
|
-
inputs = pd.DataFrame([inputs])
|
|
84
|
-
if not isinstance(inputs, pd.DataFrame):
|
|
85
|
-
raise ValueError(
|
|
86
|
-
"Input must be a DataFrame for flows with 'many' cardinality"
|
|
87
|
-
)
|
|
88
|
-
from qtype.interpreter.batch.flow import batch_execute_flow
|
|
89
|
-
|
|
90
|
-
batch_config = batch_config or BatchConfig()
|
|
91
|
-
results, errors = batch_execute_flow(
|
|
92
|
-
target_flow, inputs, batch_config, **kwargs
|
|
93
|
-
) # type: ignore
|
|
94
|
-
return results
|
|
95
|
-
else:
|
|
96
|
-
from qtype.interpreter.flow import execute_flow
|
|
97
|
-
|
|
98
|
-
args = {**kwargs, **inputs}
|
|
99
|
-
return execute_flow(target_flow, **args)
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
"""Visualize an application as Mermaid diagram."""
|
|
103
|
-
from qtype.semantic.visualize import visualize_application
|
|
109
|
+
logger.info(f"Executing flow {target_flow.id} from {path}")
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
return visualize_application(semantic_model)
|
|
111
|
+
# Convert inputs to DataFrame (normalize single dict to 1-row DataFrame)
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
wrapped_document = Document(root=document)
|
|
116
|
-
|
|
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
|
|
113
|
+
if isinstance(inputs, dict):
|
|
114
|
+
input_df = pd.DataFrame([inputs])
|
|
115
|
+
elif isinstance(inputs, pd.DataFrame):
|
|
116
|
+
input_df = inputs
|
|
117
|
+
else:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Inputs must be dict or DataFrame, got {type(inputs)}"
|
|
123
120
|
)
|
|
124
|
-
except ImportError:
|
|
125
|
-
# Fallback to basic YAML if pydantic_yaml is not available
|
|
126
|
-
import yaml
|
|
127
121
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
|
|
122
|
+
# Create session
|
|
123
|
+
session = Session(
|
|
124
|
+
session_id=kwargs.pop("session_id", "default"),
|
|
125
|
+
conversation_history=kwargs.pop("conversation_history", []),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Convert DataFrame to FlowMessages
|
|
129
|
+
initial_messages = dataframe_to_flow_messages(input_df, session)
|
|
130
|
+
|
|
131
|
+
# Execute the flow
|
|
132
|
+
secret_manager = self.secret_manager(semantic_model)
|
|
133
|
+
|
|
134
|
+
context = ExecutorContext(
|
|
135
|
+
secret_manager=secret_manager,
|
|
136
|
+
tracer=trace.get_tracer(__name__),
|
|
137
|
+
)
|
|
138
|
+
results = await run_flow(
|
|
139
|
+
target_flow,
|
|
140
|
+
initial_messages,
|
|
141
|
+
context=context,
|
|
142
|
+
**kwargs,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Convert results back to DataFrame
|
|
146
|
+
results_df = flow_messages_to_dataframe(results, target_flow)
|
|
147
|
+
|
|
148
|
+
return results_df
|
|
132
149
|
|
|
133
150
|
def generate_aws_bedrock_models(self) -> list[dict[str, Any]]:
|
|
134
151
|
"""
|
qtype/base/types.py
CHANGED
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pathlib
|
|
6
|
-
|
|
6
|
+
import types
|
|
7
|
+
import typing
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Generic,
|
|
12
|
+
Literal,
|
|
13
|
+
Optional,
|
|
14
|
+
Type,
|
|
15
|
+
TypeVar,
|
|
16
|
+
Union,
|
|
17
|
+
get_args,
|
|
18
|
+
get_origin,
|
|
19
|
+
)
|
|
7
20
|
|
|
8
21
|
from pydantic import BaseModel
|
|
9
|
-
|
|
10
|
-
from
|
|
22
|
+
from pydantic import ConfigDict as PydanticConfigDict
|
|
23
|
+
from pydantic import Field, model_validator
|
|
11
24
|
|
|
12
25
|
# JSON-serializable value types
|
|
13
26
|
JSONValue = Union[
|
|
@@ -20,10 +33,217 @@ JSONValue = Union[
|
|
|
20
33
|
list["JSONValue"],
|
|
21
34
|
]
|
|
22
35
|
|
|
23
|
-
# Configuration dictionary type
|
|
24
|
-
ConfigDict = dict[str, Any]
|
|
25
|
-
|
|
26
36
|
# Path-like type (string or Path object)
|
|
27
37
|
PathLike = Union[str, pathlib.Path]
|
|
38
|
+
|
|
28
39
|
CustomTypeRegistry = dict[str, Type[BaseModel]]
|
|
29
|
-
|
|
40
|
+
# Configuration dictionary type
|
|
41
|
+
ConfigDict = dict[str, Any]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------- Shared Base Types and Enums ----------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PrimitiveTypeEnum(str, Enum):
|
|
48
|
+
"""Represents the type of data a user or system input can accept within the DSL."""
|
|
49
|
+
|
|
50
|
+
audio = "audio"
|
|
51
|
+
boolean = "boolean"
|
|
52
|
+
bytes = "bytes"
|
|
53
|
+
citation_document = "citation_document"
|
|
54
|
+
citation_url = "citation_url"
|
|
55
|
+
date = "date"
|
|
56
|
+
datetime = "datetime"
|
|
57
|
+
int = "int"
|
|
58
|
+
file = "file"
|
|
59
|
+
float = "float"
|
|
60
|
+
image = "image"
|
|
61
|
+
text = "text"
|
|
62
|
+
time = "time"
|
|
63
|
+
video = "video"
|
|
64
|
+
thinking = "thinking"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class StepCardinality(str, Enum):
|
|
68
|
+
"""Does this step emit 1 (one) or 0...N (many) items?"""
|
|
69
|
+
|
|
70
|
+
one = "one"
|
|
71
|
+
many = "many"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
ReferenceT = TypeVar("ReferenceT")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Reference(BaseModel, Generic[ReferenceT]):
|
|
78
|
+
"""Represents a reference to another component by its ID."""
|
|
79
|
+
|
|
80
|
+
# model_config = PydanticConfigDict(extra="forbid")
|
|
81
|
+
|
|
82
|
+
ref: str = Field(..., alias="$ref")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _contains_reference_and_str(type_hint: Any) -> bool:
|
|
86
|
+
"""Check if type contains both Reference and str in a union."""
|
|
87
|
+
# Get union args (handles Union, | syntax, and Optional)
|
|
88
|
+
origin = get_origin(type_hint)
|
|
89
|
+
if origin not in (Union, None) and not isinstance(
|
|
90
|
+
type_hint, types.UnionType
|
|
91
|
+
):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
args = get_args(type_hint)
|
|
95
|
+
if not args:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
has_str = str in args
|
|
99
|
+
has_ref = any(
|
|
100
|
+
get_origin(arg) is Reference
|
|
101
|
+
or (hasattr(arg, "__mro__") and Reference in arg.__mro__)
|
|
102
|
+
for arg in args
|
|
103
|
+
)
|
|
104
|
+
return has_str and has_ref
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _should_transform_field(type_hint: Any) -> tuple[bool, bool]:
|
|
108
|
+
"""
|
|
109
|
+
Check if field should be transformed.
|
|
110
|
+
Returns: (should_transform, is_list)
|
|
111
|
+
"""
|
|
112
|
+
# Check direct union: Reference[T] | str
|
|
113
|
+
if _contains_reference_and_str(type_hint):
|
|
114
|
+
return True, False
|
|
115
|
+
|
|
116
|
+
# Check list of union: list[Reference[T] | str]
|
|
117
|
+
origin = get_origin(type_hint)
|
|
118
|
+
if origin is list:
|
|
119
|
+
args = get_args(type_hint)
|
|
120
|
+
if args and _contains_reference_and_str(args[0]):
|
|
121
|
+
return True, True
|
|
122
|
+
|
|
123
|
+
# Check optional list: list[Reference[T] | str] | None
|
|
124
|
+
if origin is Union or isinstance(type_hint, types.UnionType):
|
|
125
|
+
for arg in get_args(type_hint):
|
|
126
|
+
if get_origin(arg) is list:
|
|
127
|
+
inner_args = get_args(arg)
|
|
128
|
+
if inner_args and _contains_reference_and_str(inner_args[0]):
|
|
129
|
+
return True, True
|
|
130
|
+
|
|
131
|
+
return False, False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class StrictBaseModel(BaseModel):
|
|
135
|
+
"""Base model with extra fields forbidden."""
|
|
136
|
+
|
|
137
|
+
model_config = PydanticConfigDict(extra="forbid")
|
|
138
|
+
|
|
139
|
+
@model_validator(mode="before")
|
|
140
|
+
@classmethod
|
|
141
|
+
def normalize_string_references(cls, data: Any) -> Any:
|
|
142
|
+
"""
|
|
143
|
+
Normalize string references to Reference objects before validation.
|
|
144
|
+
|
|
145
|
+
Transforms:
|
|
146
|
+
- `field: "ref_id"` -> `field: {"$ref": "ref_id"}`
|
|
147
|
+
- `field: ["ref1", "ref2"]` -> `field: [{"$ref": "ref1"}, {"$ref": "ref2"}]`
|
|
148
|
+
|
|
149
|
+
Only applies to fields typed as `Reference[T] | str` or `list[Reference[T] | str]`.
|
|
150
|
+
"""
|
|
151
|
+
if not isinstance(data, dict):
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
# Get type hints (evaluates ForwardRefs)
|
|
155
|
+
hints = typing.get_type_hints(cls)
|
|
156
|
+
|
|
157
|
+
# Transform fields
|
|
158
|
+
for field_name, field_value in data.items():
|
|
159
|
+
if field_name == "type" or field_name not in hints:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
should_transform, is_list = _should_transform_field(
|
|
163
|
+
hints[field_name]
|
|
164
|
+
)
|
|
165
|
+
if not should_transform:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if is_list and isinstance(field_value, list):
|
|
169
|
+
data[field_name] = [
|
|
170
|
+
{"$ref": item} if isinstance(item, str) else item
|
|
171
|
+
for item in field_value
|
|
172
|
+
]
|
|
173
|
+
elif not is_list and isinstance(field_value, str):
|
|
174
|
+
data[field_name] = {"$ref": field_value}
|
|
175
|
+
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class BatchConfig(BaseModel):
|
|
180
|
+
"""Configuration for batch execution.
|
|
181
|
+
|
|
182
|
+
Attributes:
|
|
183
|
+
num_workers: Number of async workers for batch operations.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
batch_size: int = Field(
|
|
187
|
+
default=25,
|
|
188
|
+
description="Max number of rows to send to a step at a time",
|
|
189
|
+
gt=0,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ConcurrencyConfig(BaseModel):
|
|
194
|
+
"""Configuration for concurrent processing.
|
|
195
|
+
|
|
196
|
+
Attributes:
|
|
197
|
+
num_workers: Number of async workers for batch operations.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
num_workers: int = Field(
|
|
201
|
+
default=1,
|
|
202
|
+
description="Number of async workers for batch operations",
|
|
203
|
+
gt=0,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class BatchableStepMixin(BaseModel):
|
|
208
|
+
"""A mixin for steps that support concurrent batch processing."""
|
|
209
|
+
|
|
210
|
+
batch_config: BatchConfig = Field(
|
|
211
|
+
default_factory=BatchConfig,
|
|
212
|
+
description="Configuration for processing the input stream in batches. If omitted, the step processes items one by one.",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class CacheConfig(BaseModel):
|
|
217
|
+
directory: PathLike = Field(
|
|
218
|
+
default=pathlib.Path("./.qtype-cache"),
|
|
219
|
+
description="Base cache directory.",
|
|
220
|
+
)
|
|
221
|
+
namespace: Optional[str] = Field(
|
|
222
|
+
default=None, description="Logical namespace for cache keys."
|
|
223
|
+
)
|
|
224
|
+
on_error: Literal["Cache", "Drop"] = "Drop"
|
|
225
|
+
version: str = Field(
|
|
226
|
+
default="1.0", description="Bump to invalidate old cache."
|
|
227
|
+
)
|
|
228
|
+
compress: bool = Field(default=False, description="Compress stored data.")
|
|
229
|
+
ttl: Optional[int] = Field(
|
|
230
|
+
default=None, description="Optional time-to-live in seconds."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CachedStepMixin(BaseModel):
|
|
235
|
+
"""A mixin for steps that support caching."""
|
|
236
|
+
|
|
237
|
+
cache_config: CacheConfig | None = Field(
|
|
238
|
+
default=None,
|
|
239
|
+
description="Configuration for caching step outputs. If omitted, caching is disabled.",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ConcurrentStepMixin(BaseModel):
|
|
244
|
+
"""A mixin for steps that support concurrent processing."""
|
|
245
|
+
|
|
246
|
+
concurrency_config: ConcurrencyConfig = Field(
|
|
247
|
+
default_factory=ConcurrencyConfig,
|
|
248
|
+
description="Configuration for processing the input stream concurrently. If omitted, the step processes items sequentially.",
|
|
249
|
+
)
|
qtype/cli.py
CHANGED
|
@@ -7,6 +7,10 @@ import importlib
|
|
|
7
7
|
import logging
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from qtype.base.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("application.facade")
|
|
13
|
+
|
|
10
14
|
try:
|
|
11
15
|
from importlib.metadata import entry_points
|
|
12
16
|
except ImportError:
|
|
@@ -131,7 +135,7 @@ def main() -> None:
|
|
|
131
135
|
# Set logging level based on user input
|
|
132
136
|
logging.basicConfig(
|
|
133
137
|
level=getattr(logging, args.log_level),
|
|
134
|
-
format="%(levelname)s: %(message)s",
|
|
138
|
+
format="%(asctime)s - %(levelname)s: %(message)s",
|
|
135
139
|
)
|
|
136
140
|
|
|
137
141
|
# Dispatch to the selected subcommand
|
qtype/commands/convert.py
CHANGED
|
@@ -8,14 +8,62 @@ import argparse
|
|
|
8
8
|
import logging
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from qtype.
|
|
11
|
+
from qtype.dsl.model import Application, Document, ToolList
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _convert_to_yaml(doc: Application | ToolList) -> str:
|
|
17
|
+
"""Convert a document to YAML format."""
|
|
18
|
+
from pydantic_yaml import to_yaml_str
|
|
19
|
+
|
|
20
|
+
# Wrap in Document if needed
|
|
21
|
+
if isinstance(doc, Application):
|
|
22
|
+
wrapped = Document(root=doc)
|
|
23
|
+
else:
|
|
24
|
+
wrapped = doc
|
|
25
|
+
|
|
26
|
+
# NOTE: We use exclude_none but NOT exclude_unset because discriminator
|
|
27
|
+
# fields like 'type' have default values and must be included in output
|
|
28
|
+
return to_yaml_str(wrapped, exclude_none=True)
|
|
29
|
+
|
|
30
|
+
|
|
16
31
|
def convert_api(args: argparse.Namespace) -> None:
|
|
17
32
|
"""Convert API specification to qtype format."""
|
|
18
|
-
|
|
33
|
+
from qtype.application.converters.tools_from_api import tools_from_api
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
api_name, auths, tools, types = tools_from_api(args.api_spec)
|
|
37
|
+
if not tools:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"No tools found from the API specification: {args.api_spec}"
|
|
40
|
+
)
|
|
41
|
+
if not auths and not types:
|
|
42
|
+
doc = ToolList(
|
|
43
|
+
root=list(tools),
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
doc: Application | ToolList = Application(
|
|
47
|
+
id=api_name,
|
|
48
|
+
description=f"Tools created from API specification {args.api_spec}",
|
|
49
|
+
tools=list(tools),
|
|
50
|
+
types=types,
|
|
51
|
+
auths=auths,
|
|
52
|
+
)
|
|
53
|
+
# Convert to YAML format
|
|
54
|
+
content = _convert_to_yaml(doc)
|
|
55
|
+
|
|
56
|
+
# Write to file or stdout
|
|
57
|
+
if args.output:
|
|
58
|
+
output_path = Path(args.output)
|
|
59
|
+
output_path.write_text(content, encoding="utf-8")
|
|
60
|
+
logger.info(f"✅ Converted tools saved to {output_path}")
|
|
61
|
+
else:
|
|
62
|
+
print(content)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"❌ Conversion failed: {e}")
|
|
66
|
+
raise
|
|
19
67
|
|
|
20
68
|
|
|
21
69
|
def convert_module(args: argparse.Namespace) -> None:
|
|
@@ -23,7 +71,6 @@ def convert_module(args: argparse.Namespace) -> None:
|
|
|
23
71
|
from qtype.application.converters.tools_from_module import (
|
|
24
72
|
tools_from_module,
|
|
25
73
|
)
|
|
26
|
-
from qtype.dsl.model import Application, ToolList
|
|
27
74
|
|
|
28
75
|
try:
|
|
29
76
|
tools, types = tools_from_module(args.module_path)
|
|
@@ -45,9 +92,8 @@ def convert_module(args: argparse.Namespace) -> None:
|
|
|
45
92
|
root=list(tools),
|
|
46
93
|
)
|
|
47
94
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
content = facade.convert_document(doc)
|
|
95
|
+
# Convert to YAML format
|
|
96
|
+
content = _convert_to_yaml(doc)
|
|
51
97
|
|
|
52
98
|
# Write to file or stdout
|
|
53
99
|
if args.output:
|
qtype/commands/generate.py
CHANGED
|
@@ -84,8 +84,44 @@ def generate_schema(args: argparse.Namespace) -> None:
|
|
|
84
84
|
'output' attribute specifying the output file path.
|
|
85
85
|
"""
|
|
86
86
|
schema = Document.model_json_schema()
|
|
87
|
+
|
|
87
88
|
# Add the $schema property to indicate JSON Schema version
|
|
88
89
|
schema["$schema"] = "http://json-schema.org/draft-07/schema#"
|
|
90
|
+
|
|
91
|
+
# Add custom YAML tag definitions for QType loader features
|
|
92
|
+
if "$defs" not in schema:
|
|
93
|
+
schema["$defs"] = {}
|
|
94
|
+
|
|
95
|
+
# Note: Custom YAML tags (!include, !include_raw) and environment variable
|
|
96
|
+
# substitution (${VAR}) are handled by the QType YAML loader at parse time,
|
|
97
|
+
# not by JSON Schema validation. We define them in $defs for documentation
|
|
98
|
+
# purposes, but we don't apply them to string fields since:
|
|
99
|
+
# 1. They would cause false positives (e.g., "localhost" matching as valid)
|
|
100
|
+
# 2. The YAML loader processes these before schema validation occurs
|
|
101
|
+
# 3. After YAML loading, the schema sees the resolved/substituted values
|
|
102
|
+
#
|
|
103
|
+
# Schema validation happens on the post-processed document structure,
|
|
104
|
+
# so we don't need to (and shouldn't) validate the raw YAML tag syntax.
|
|
105
|
+
|
|
106
|
+
# Define custom YAML tags used by QType loader
|
|
107
|
+
schema["$defs"]["qtype_include_tag"] = {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"pattern": "^!include\\s+.+",
|
|
110
|
+
"description": "Include external YAML file using QType's !include tag",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
schema["$defs"]["qtype_include_raw_tag"] = {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"pattern": "^!include_raw\\s+.+",
|
|
116
|
+
"description": "Include raw text file using QType's !include_raw tag",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
schema["$defs"]["qtype_env_var"] = {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"pattern": "^.*\\$\\{[^}:]+(?::[^}]*)?\\}.*$",
|
|
122
|
+
"description": "String with environment variable substitution using ${VAR_NAME} or ${VAR_NAME:default} syntax",
|
|
123
|
+
}
|
|
124
|
+
|
|
89
125
|
output = json.dumps(schema, indent=2)
|
|
90
126
|
output_path: Optional[str] = getattr(args, "output", None)
|
|
91
127
|
if output_path:
|
|
@@ -150,6 +186,14 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
150
186
|
import networkx # noqa: F401
|
|
151
187
|
import ruff # type: ignore[import-untyped] # noqa: F401
|
|
152
188
|
|
|
189
|
+
has_semantic_deps = True
|
|
190
|
+
except ImportError:
|
|
191
|
+
logger.debug(
|
|
192
|
+
"NetworkX or Ruff is not installed. Skipping semantic model generation."
|
|
193
|
+
)
|
|
194
|
+
has_semantic_deps = False
|
|
195
|
+
|
|
196
|
+
if has_semantic_deps:
|
|
153
197
|
from qtype.semantic.generate import generate_semantic_model
|
|
154
198
|
|
|
155
199
|
semantic_parser = generate_subparsers.add_parser(
|
|
@@ -164,7 +208,3 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
164
208
|
help="Output file for the semantic model (default: stdout)",
|
|
165
209
|
)
|
|
166
210
|
semantic_parser.set_defaults(func=generate_semantic_model)
|
|
167
|
-
except ImportError:
|
|
168
|
-
logger.debug(
|
|
169
|
-
"NetworkX or Ruff is not installed. Skipping semantic model generation."
|
|
170
|
-
)
|