qtype 0.0.15__py3-none-any.whl → 0.1.0__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 +5 -5
- qtype/application/converters/tools_from_module.py +2 -2
- qtype/application/converters/types.py +14 -43
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +92 -71
- qtype/base/types.py +227 -7
- qtype/commands/convert.py +20 -8
- qtype/commands/generate.py +19 -27
- qtype/commands/run.py +54 -36
- qtype/commands/serve.py +74 -54
- qtype/commands/validate.py +34 -8
- qtype/commands/visualize.py +46 -22
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +65 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +612 -363
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +58 -135
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +429 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +74 -0
- qtype/interpreter/base/factory.py +117 -0
- qtype/interpreter/base/progress_tracker.py +75 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +73 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +455 -21
- qtype/interpreter/converters.py +73 -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/decoder_executor.py +163 -0
- qtype/interpreter/executors/doc_to_text_executor.py +112 -0
- qtype/interpreter/executors/document_embedder_executor.py +75 -0
- qtype/interpreter/executors/document_search_executor.py +122 -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 +160 -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 +228 -0
- qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
- qtype/interpreter/executors/invoke_flow_executor.py +51 -0
- qtype/interpreter/executors/invoke_tool_executor.py +353 -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 +147 -22
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- 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 +328 -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/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
- 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/css/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +4 -4
- qtype/semantic/checker.py +583 -0
- qtype/semantic/generate.py +262 -83
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +436 -159
- qtype/semantic/resolver.py +59 -17
- qtype/semantic/visualize.py +28 -31
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/METADATA +16 -3
- qtype-0.1.0.dist-info/RECORD +134 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -465
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/file_sink_source.py +0 -162
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -92
- qtype/interpreter/batch/step.py +0 -74
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -178
- 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 -171
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -219
- qtype/interpreter/streaming_helpers.py +0 -123
- qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
- qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/loader.py +0 -390
- qtype-0.0.15.dist-info/RECORD +0 -106
- /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/WHEEL +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/top_level.txt +0 -0
qtype/commands/generate.py
CHANGED
|
@@ -92,6 +92,17 @@ def generate_schema(args: argparse.Namespace) -> None:
|
|
|
92
92
|
if "$defs" not in schema:
|
|
93
93
|
schema["$defs"] = {}
|
|
94
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
|
+
|
|
95
106
|
# Define custom YAML tags used by QType loader
|
|
96
107
|
schema["$defs"]["qtype_include_tag"] = {
|
|
97
108
|
"type": "string",
|
|
@@ -111,29 +122,6 @@ def generate_schema(args: argparse.Namespace) -> None:
|
|
|
111
122
|
"description": "String with environment variable substitution using ${VAR_NAME} or ${VAR_NAME:default} syntax",
|
|
112
123
|
}
|
|
113
124
|
|
|
114
|
-
# Add these custom patterns to string types throughout the schema
|
|
115
|
-
def add_custom_patterns(obj):
|
|
116
|
-
if isinstance(obj, dict):
|
|
117
|
-
if obj.get("type") == "string" and "anyOf" not in obj:
|
|
118
|
-
# Add anyOf to allow either regular strings or custom tag patterns
|
|
119
|
-
original_obj = obj.copy()
|
|
120
|
-
obj.clear()
|
|
121
|
-
obj["anyOf"] = [
|
|
122
|
-
original_obj,
|
|
123
|
-
{"$ref": "#/$defs/qtype_include_tag"},
|
|
124
|
-
{"$ref": "#/$defs/qtype_include_raw_tag"},
|
|
125
|
-
{"$ref": "#/$defs/qtype_env_var"},
|
|
126
|
-
]
|
|
127
|
-
else:
|
|
128
|
-
for value in obj.values():
|
|
129
|
-
add_custom_patterns(value)
|
|
130
|
-
elif isinstance(obj, list):
|
|
131
|
-
for item in obj:
|
|
132
|
-
add_custom_patterns(item)
|
|
133
|
-
|
|
134
|
-
# Apply custom patterns to the schema
|
|
135
|
-
add_custom_patterns(schema)
|
|
136
|
-
|
|
137
125
|
output = json.dumps(schema, indent=2)
|
|
138
126
|
output_path: Optional[str] = getattr(args, "output", None)
|
|
139
127
|
if output_path:
|
|
@@ -198,6 +186,14 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
198
186
|
import networkx # noqa: F401
|
|
199
187
|
import ruff # type: ignore[import-untyped] # noqa: F401
|
|
200
188
|
|
|
189
|
+
has_semantic_deps = True
|
|
190
|
+
except ImportError:
|
|
191
|
+
logger.warning(
|
|
192
|
+
"NetworkX or Ruff is not installed. Skipping semantic model generation."
|
|
193
|
+
)
|
|
194
|
+
has_semantic_deps = False
|
|
195
|
+
|
|
196
|
+
if has_semantic_deps:
|
|
201
197
|
from qtype.semantic.generate import generate_semantic_model
|
|
202
198
|
|
|
203
199
|
semantic_parser = generate_subparsers.add_parser(
|
|
@@ -212,7 +208,3 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
212
208
|
help="Output file for the semantic model (default: stdout)",
|
|
213
209
|
)
|
|
214
210
|
semantic_parser.set_defaults(func=generate_semantic_model)
|
|
215
|
-
except ImportError:
|
|
216
|
-
logger.debug(
|
|
217
|
-
"NetworkX or Ruff is not installed. Skipping semantic model generation."
|
|
218
|
-
)
|
qtype/commands/run.py
CHANGED
|
@@ -10,7 +10,6 @@ import logging
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
-
import magic
|
|
14
13
|
import pandas as pd
|
|
15
14
|
|
|
16
15
|
from qtype.application.facade import QTypeFacade
|
|
@@ -23,10 +22,28 @@ def read_data_from_file(file_path: str) -> pd.DataFrame:
|
|
|
23
22
|
"""
|
|
24
23
|
Reads a file into a pandas DataFrame based on its MIME type.
|
|
25
24
|
"""
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import magic
|
|
28
|
+
|
|
26
29
|
mime_type = magic.Magic(mime=True).from_file(file_path)
|
|
27
30
|
|
|
28
31
|
if mime_type == "text/csv":
|
|
29
32
|
return pd.read_csv(file_path)
|
|
33
|
+
elif mime_type == "text/plain":
|
|
34
|
+
# For text/plain, use file extension to determine format
|
|
35
|
+
file_ext = Path(file_path).suffix.lower()
|
|
36
|
+
if file_ext == ".csv":
|
|
37
|
+
return pd.read_csv(file_path)
|
|
38
|
+
elif file_ext == ".json":
|
|
39
|
+
return pd.read_json(file_path)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
(
|
|
43
|
+
f"Unsupported text/plain file extension: {file_ext}. "
|
|
44
|
+
"Supported extensions: .csv, .json"
|
|
45
|
+
)
|
|
46
|
+
)
|
|
30
47
|
elif mime_type == "application/json":
|
|
31
48
|
return pd.read_json(file_path)
|
|
32
49
|
elif mime_type in [
|
|
@@ -48,6 +65,8 @@ def run_flow(args: Any) -> None:
|
|
|
48
65
|
Args:
|
|
49
66
|
args: Arguments passed from the command line or calling context.
|
|
50
67
|
"""
|
|
68
|
+
import asyncio
|
|
69
|
+
|
|
51
70
|
facade = QTypeFacade()
|
|
52
71
|
spec_path = Path(args.spec)
|
|
53
72
|
|
|
@@ -65,54 +84,53 @@ def run_flow(args: Any) -> None:
|
|
|
65
84
|
logger.error(f"❌ Invalid JSON input: {e}")
|
|
66
85
|
return
|
|
67
86
|
|
|
68
|
-
# Execute the workflow using the facade
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
# Execute the workflow using the facade (now async, returns DataFrame)
|
|
88
|
+
result_df = asyncio.run(
|
|
89
|
+
facade.execute_workflow(
|
|
90
|
+
spec_path, flow_name=args.flow, inputs=input
|
|
91
|
+
)
|
|
71
92
|
)
|
|
72
93
|
|
|
73
94
|
logger.info("✅ Flow execution completed successfully")
|
|
74
95
|
|
|
75
|
-
#
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
# Display results
|
|
97
|
+
if len(result_df) > 0:
|
|
98
|
+
logger.info(f"Processed {len(result_df)} input(s)")
|
|
99
|
+
|
|
100
|
+
# Remove 'row' and 'error' columns for display if all errors are None
|
|
101
|
+
display_df = result_df.copy()
|
|
102
|
+
if (
|
|
103
|
+
"error" in display_df.columns
|
|
104
|
+
and display_df["error"].isna().all()
|
|
105
|
+
):
|
|
106
|
+
display_df = display_df.drop(columns=["error"])
|
|
107
|
+
if "row" in display_df.columns:
|
|
108
|
+
display_df = display_df.drop(columns=["row"])
|
|
109
|
+
|
|
110
|
+
if len(display_df) > 1:
|
|
111
|
+
logger.info(f"\nResults:\n{display_df.to_string()}")
|
|
112
|
+
else:
|
|
113
|
+
# Print the first row with column_name: value one per line
|
|
114
|
+
fmt_str = []
|
|
115
|
+
for col, val in display_df.iloc[0].items():
|
|
116
|
+
fmt_str.append(f"{col}: {val}")
|
|
117
|
+
fmt_str = "\n".join(fmt_str)
|
|
118
|
+
logger.info(f"\nResults:\n{fmt_str}")
|
|
119
|
+
|
|
120
|
+
# Save the output
|
|
121
|
+
if args.output:
|
|
122
|
+
# Save full DataFrame with row and error columns
|
|
123
|
+
result_df.to_parquet(args.output)
|
|
124
|
+
logger.info(f"Output saved to {args.output}")
|
|
95
125
|
else:
|
|
96
126
|
logger.info("Flow completed with no output")
|
|
97
127
|
|
|
98
|
-
# save the output
|
|
99
|
-
if isinstance(result, pd.DataFrame) and args.output:
|
|
100
|
-
result.to_parquet(args.output)
|
|
101
|
-
logger.info(f"Output DataFrame saved to {args.output}")
|
|
102
|
-
elif args.output:
|
|
103
|
-
with open(args.output, "w") as f:
|
|
104
|
-
json.dump(result, f, indent=2)
|
|
105
|
-
logger.info(f"Output saved to {args.output}")
|
|
106
|
-
|
|
107
128
|
except LoadError as e:
|
|
108
129
|
logger.error(f"❌ Failed to load document: {e}")
|
|
109
130
|
except ValidationError as e:
|
|
110
131
|
logger.error(f"❌ Validation failed: {e}")
|
|
111
132
|
except InterpreterError as e:
|
|
112
133
|
logger.error(f"❌ Execution failed: {e}")
|
|
113
|
-
except Exception as e:
|
|
114
|
-
logger.error(f"❌ Unexpected error: {e}", exc_info=True)
|
|
115
|
-
pass
|
|
116
134
|
|
|
117
135
|
|
|
118
136
|
def parser(subparsers: argparse._SubParsersAction) -> None:
|
qtype/commands/serve.py
CHANGED
|
@@ -6,71 +6,91 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import logging
|
|
9
|
+
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import uvicorn
|
|
13
14
|
|
|
14
|
-
from qtype.
|
|
15
|
-
from qtype.
|
|
15
|
+
from qtype.base.exceptions import ValidationError
|
|
16
|
+
from qtype.semantic.loader import load
|
|
17
|
+
from qtype.semantic.model import Application
|
|
16
18
|
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
def
|
|
22
|
+
def create_api_app() -> Any:
|
|
23
|
+
"""Factory function to create FastAPI app.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
FastAPI application instance.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
RuntimeError: If QTYPE_SPEC_PATH not set in environment.
|
|
30
|
+
ValidationError: If spec is not an Application document.
|
|
31
|
+
"""
|
|
32
|
+
from qtype.interpreter.api import APIExecutor
|
|
33
|
+
|
|
34
|
+
spec_path_str = os.environ["_QTYPE_SPEC_PATH"]
|
|
35
|
+
|
|
36
|
+
spec_path = Path(spec_path_str)
|
|
37
|
+
logger.info(f"Loading spec: {spec_path}")
|
|
38
|
+
|
|
39
|
+
semantic_model, _ = load(spec_path)
|
|
40
|
+
if not isinstance(semantic_model, Application):
|
|
41
|
+
raise ValidationError("Can only serve Application documents")
|
|
42
|
+
|
|
43
|
+
logger.info(f"✅ Successfully loaded spec: {spec_path}")
|
|
44
|
+
|
|
45
|
+
# Derive name from spec filename
|
|
46
|
+
name = spec_path.name.replace(".qtype.yaml", "").replace("_", " ").title()
|
|
47
|
+
|
|
48
|
+
# Get host/port from environment (set by uvicorn)
|
|
49
|
+
host = os.environ["_QTYPE_HOST"]
|
|
50
|
+
port = int(os.environ["_QTYPE_PORT"])
|
|
51
|
+
|
|
52
|
+
# Create server info for OpenAPI spec
|
|
53
|
+
servers = [
|
|
54
|
+
{
|
|
55
|
+
"url": f"http://{host}:{port}",
|
|
56
|
+
"description": "Development server",
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
api_executor = APIExecutor(semantic_model, host, port)
|
|
61
|
+
return api_executor.create_app(
|
|
62
|
+
name=name,
|
|
63
|
+
ui_enabled=True,
|
|
64
|
+
servers=servers,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def serve(args: argparse.Namespace) -> None:
|
|
21
69
|
"""Run a QType YAML spec file as an API.
|
|
22
70
|
|
|
23
71
|
Args:
|
|
24
|
-
args: Arguments passed from the command line
|
|
72
|
+
args: Arguments passed from the command line.
|
|
25
73
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
api_executor = APIExecutor(semantic_model)
|
|
47
|
-
|
|
48
|
-
# Create server info for OpenAPI spec
|
|
49
|
-
servers = [
|
|
50
|
-
{
|
|
51
|
-
"url": f"http://{args.host}:{args.port}",
|
|
52
|
-
"description": "Development server",
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
fastapi_app = api_executor.create_app(
|
|
57
|
-
name=name, ui_enabled=not args.disable_ui, servers=servers
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
# Start the server
|
|
61
|
-
uvicorn.run(
|
|
62
|
-
fastapi_app, host=args.host, port=args.port, log_level="info"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
except LoadError as e:
|
|
66
|
-
logger.error(f"❌ Failed to load document: {e}")
|
|
67
|
-
exit(1)
|
|
68
|
-
except ValidationError as e:
|
|
69
|
-
logger.error(f"❌ Validation failed: {e}")
|
|
70
|
-
exit(1)
|
|
71
|
-
except Exception as e:
|
|
72
|
-
logger.error(f"❌ Unexpected error starting server: {e}")
|
|
73
|
-
exit(1)
|
|
74
|
+
# Set environment variables for factory function
|
|
75
|
+
os.environ["_QTYPE_SPEC_PATH"] = args.spec
|
|
76
|
+
os.environ["_QTYPE_HOST"] = args.host
|
|
77
|
+
os.environ["_QTYPE_PORT"] = str(args.port)
|
|
78
|
+
|
|
79
|
+
logger.info(
|
|
80
|
+
f"Starting server on {args.host}:{args.port}"
|
|
81
|
+
f"{' (reload enabled)' if args.reload else ''}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Use factory mode with import string
|
|
85
|
+
uvicorn.run(
|
|
86
|
+
"qtype.commands.serve:create_api_app",
|
|
87
|
+
factory=True,
|
|
88
|
+
host=args.host,
|
|
89
|
+
port=args.port,
|
|
90
|
+
log_level="info",
|
|
91
|
+
reload=args.reload,
|
|
92
|
+
reload_includes=[args.spec] if args.reload else None,
|
|
93
|
+
)
|
|
74
94
|
|
|
75
95
|
|
|
76
96
|
def parser(subparsers: argparse._SubParsersAction) -> None:
|
|
@@ -86,9 +106,9 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
86
106
|
cmd_parser.add_argument("-p", "--port", type=int, default=8000)
|
|
87
107
|
cmd_parser.add_argument("-H", "--host", type=str, default="localhost")
|
|
88
108
|
cmd_parser.add_argument(
|
|
89
|
-
"--
|
|
109
|
+
"--reload",
|
|
90
110
|
action="store_true",
|
|
91
|
-
help="
|
|
111
|
+
help="Enable auto-reload on code changes (default: False).",
|
|
92
112
|
)
|
|
93
113
|
cmd_parser.set_defaults(func=serve)
|
|
94
114
|
|
qtype/commands/validate.py
CHANGED
|
@@ -10,9 +10,12 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
-
from qtype import dsl
|
|
14
|
-
from qtype.application.facade import QTypeFacade
|
|
15
13
|
from qtype.base.exceptions import LoadError, SemanticError, ValidationError
|
|
14
|
+
from qtype.dsl.linker import DuplicateComponentError, ReferenceNotFoundError
|
|
15
|
+
from qtype.dsl.loader import YAMLLoadError
|
|
16
|
+
from qtype.dsl.model import Application as DSLApplication
|
|
17
|
+
from qtype.dsl.model import Document
|
|
18
|
+
from qtype.semantic.loader import load
|
|
16
19
|
|
|
17
20
|
logger = logging.getLogger(__name__)
|
|
18
21
|
|
|
@@ -27,19 +30,33 @@ def main(args: Any) -> None:
|
|
|
27
30
|
Exits:
|
|
28
31
|
Exits with code 1 if validation fails.
|
|
29
32
|
"""
|
|
30
|
-
facade = QTypeFacade()
|
|
31
33
|
spec_path = Path(args.spec)
|
|
32
34
|
|
|
33
35
|
try:
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
loaded_data, custom_types = facade.load_semantic_model(spec_path)
|
|
36
|
+
# Load and validate the document (works for all document types)
|
|
37
|
+
# This includes: YAML parsing, Pydantic validation, linking, and semantic checks
|
|
38
|
+
loaded_data, custom_types = load(spec_path)
|
|
38
39
|
logger.info("✅ Validation successful - document is valid.")
|
|
39
40
|
|
|
40
41
|
except LoadError as e:
|
|
41
42
|
logger.error(f"❌ Failed to load document: {e}")
|
|
42
43
|
sys.exit(1)
|
|
44
|
+
except YAMLLoadError as e:
|
|
45
|
+
# YAML syntax errors
|
|
46
|
+
logger.error(f"❌ {e}")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
except DuplicateComponentError as e:
|
|
49
|
+
# Duplicate ID errors during linking
|
|
50
|
+
logger.error(f"❌ {e}")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
except ReferenceNotFoundError as e:
|
|
53
|
+
# Reference resolution errors during linking
|
|
54
|
+
logger.error(f"❌ {e}")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
# Pydantic validation errors from parse_document
|
|
58
|
+
logger.error(f"❌ {e}")
|
|
59
|
+
sys.exit(1)
|
|
43
60
|
except ValidationError as e:
|
|
44
61
|
logger.error(f"❌ Validation failed: {e}")
|
|
45
62
|
sys.exit(1)
|
|
@@ -49,7 +66,16 @@ def main(args: Any) -> None:
|
|
|
49
66
|
|
|
50
67
|
# If printing is requested, load and print the document
|
|
51
68
|
if args.print:
|
|
52
|
-
|
|
69
|
+
from pydantic_yaml import to_yaml_str
|
|
70
|
+
|
|
71
|
+
# Wrap in Document if it's a DSL Application
|
|
72
|
+
if isinstance(loaded_data, DSLApplication):
|
|
73
|
+
wrapped = Document(root=loaded_data)
|
|
74
|
+
else:
|
|
75
|
+
wrapped = loaded_data
|
|
76
|
+
logging.info(
|
|
77
|
+
to_yaml_str(wrapped, exclude_unset=True, exclude_none=True)
|
|
78
|
+
)
|
|
53
79
|
|
|
54
80
|
|
|
55
81
|
def parser(subparsers: argparse._SubParsersAction) -> None:
|
qtype/commands/visualize.py
CHANGED
|
@@ -6,13 +6,17 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import logging
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
9
11
|
import tempfile
|
|
10
12
|
import webbrowser
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
14
|
-
from qtype.application.facade import QTypeFacade
|
|
15
16
|
from qtype.base.exceptions import LoadError, ValidationError
|
|
17
|
+
from qtype.semantic.loader import load
|
|
18
|
+
from qtype.semantic.model import Application
|
|
19
|
+
from qtype.semantic.visualize import visualize_application
|
|
16
20
|
|
|
17
21
|
logger = logging.getLogger(__name__)
|
|
18
22
|
|
|
@@ -27,12 +31,13 @@ def main(args: Any) -> None:
|
|
|
27
31
|
Exits:
|
|
28
32
|
Exits with code 1 if visualization fails.
|
|
29
33
|
"""
|
|
30
|
-
facade = QTypeFacade()
|
|
31
34
|
spec_path = Path(args.spec)
|
|
32
35
|
|
|
33
36
|
try:
|
|
34
|
-
#
|
|
35
|
-
|
|
37
|
+
# Load and generate visualization
|
|
38
|
+
semantic_model, _ = load(spec_path)
|
|
39
|
+
assert isinstance(semantic_model, Application)
|
|
40
|
+
mermaid_content = visualize_application(semantic_model)
|
|
36
41
|
|
|
37
42
|
if args.output:
|
|
38
43
|
# Write to file
|
|
@@ -41,26 +46,45 @@ def main(args: Any) -> None:
|
|
|
41
46
|
logger.info(f"✅ Visualization saved to {output_path}")
|
|
42
47
|
|
|
43
48
|
if not args.no_display:
|
|
44
|
-
#
|
|
49
|
+
# Check if mmdc is available
|
|
50
|
+
if shutil.which("mmdc") is None:
|
|
51
|
+
logger.error(
|
|
52
|
+
"❌ mmdc command not found. Please install mermaid-cli."
|
|
53
|
+
)
|
|
54
|
+
logger.info(
|
|
55
|
+
"Install with: npm install -g @mermaid-js/mermaid-cli"
|
|
56
|
+
)
|
|
57
|
+
exit(1)
|
|
58
|
+
|
|
59
|
+
# Create temporary directory and run mmdc from within it
|
|
45
60
|
try:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
# Create a temporary directory
|
|
62
|
+
temp_dir = tempfile.mkdtemp()
|
|
63
|
+
temp_dir_path = Path(temp_dir)
|
|
64
|
+
|
|
65
|
+
# Write mermaid file with simple names in the temp directory
|
|
66
|
+
mmd_file_path = temp_dir_path / "diagram.mmd"
|
|
67
|
+
svg_file_path = temp_dir_path / "diagram.svg"
|
|
68
|
+
|
|
69
|
+
mmd_file_path.write_text(mermaid_content, encoding="utf-8")
|
|
70
|
+
|
|
71
|
+
# Run mmdc from within the temporary directory
|
|
72
|
+
subprocess.run(
|
|
73
|
+
["mmdc", "-i", "diagram.mmd", "-o", "diagram.svg"],
|
|
74
|
+
cwd=temp_dir,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
check=True,
|
|
62
78
|
)
|
|
63
|
-
|
|
79
|
+
|
|
80
|
+
logger.info(
|
|
81
|
+
f"Opening visualization in browser: {svg_file_path}"
|
|
82
|
+
)
|
|
83
|
+
webbrowser.open(f"file://{svg_file_path}")
|
|
84
|
+
|
|
85
|
+
except subprocess.CalledProcessError as e:
|
|
86
|
+
logger.error(f"❌ Failed to generate SVG: {e.stderr}")
|
|
87
|
+
exit(1)
|
|
64
88
|
|
|
65
89
|
except LoadError as e:
|
|
66
90
|
logger.error(f"❌ Failed to load document: {e}")
|
qtype/dsl/__init__.py
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from .
|
|
5
|
+
# Import all public symbols from submodules
|
|
6
|
+
# The star import is intentional here - it's controlled by __all__ in each module
|
|
7
|
+
from qtype.base.types import Reference # noqa: F401
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
from .domain_types import * # noqa: F403, F401
|
|
10
|
+
from .loader import YAMLLoadError # noqa: F401
|
|
11
|
+
from .model import * # noqa: F403, F401
|
qtype/dsl/custom_types.py
CHANGED
|
@@ -2,7 +2,7 @@ from typing import Any, ForwardRef, Type, Union
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, create_model
|
|
4
4
|
|
|
5
|
-
from qtype.
|
|
5
|
+
from qtype.dsl.types import PRIMITIVE_TO_PYTHON_TYPE
|
|
6
6
|
|
|
7
7
|
# --- This would be in your interpreter's logic ---
|
|
8
8
|
|
qtype/dsl/domain_types.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from pydantic import Field
|
|
7
7
|
|
|
8
|
-
from qtype.
|
|
8
|
+
from qtype.base.types import PrimitiveTypeEnum, StrictBaseModel
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Embedding(StrictBaseModel):
|
|
@@ -14,11 +14,12 @@ class Embedding(StrictBaseModel):
|
|
|
14
14
|
vector: list[float] = Field(
|
|
15
15
|
..., description="The vector representation of the embedding."
|
|
16
16
|
)
|
|
17
|
-
|
|
18
|
-
None, description="The original
|
|
17
|
+
content: Any | None = Field(
|
|
18
|
+
None, description="The original content that was embedded."
|
|
19
19
|
)
|
|
20
|
-
metadata: dict[str,
|
|
21
|
-
|
|
20
|
+
metadata: dict[str, Any] = Field(
|
|
21
|
+
default_factory=dict,
|
|
22
|
+
description="Metadata associated with the embedding.",
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
|
|
@@ -57,3 +58,62 @@ class ChatMessage(StrictBaseModel):
|
|
|
57
58
|
...,
|
|
58
59
|
description="The content blocks of the chat message, which can include text, images, or other media.",
|
|
59
60
|
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RAGDocument(StrictBaseModel):
|
|
64
|
+
"""A standard, built-in representation of a document used in Retrieval-Augmented Generation (RAG)."""
|
|
65
|
+
|
|
66
|
+
content: Any = Field(..., description="The main content of the document.")
|
|
67
|
+
file_id: str = Field(..., description="An unique identifier for the file.")
|
|
68
|
+
file_name: str = Field(..., description="The name of the file.")
|
|
69
|
+
uri: str | None = Field(
|
|
70
|
+
None, description="The URI where the document can be found."
|
|
71
|
+
)
|
|
72
|
+
metadata: dict[str, Any] = Field(
|
|
73
|
+
default_factory=dict,
|
|
74
|
+
description="Metadata associated with the document.",
|
|
75
|
+
)
|
|
76
|
+
type: PrimitiveTypeEnum = Field(
|
|
77
|
+
...,
|
|
78
|
+
description="The type of the document content (e.g., 'text', 'image').",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RAGChunk(Embedding):
|
|
83
|
+
"""A standard, built-in representation of a chunk of a document used in Retrieval-Augmented Generation (RAG)."""
|
|
84
|
+
|
|
85
|
+
chunk_id: str = Field(
|
|
86
|
+
..., description="An unique identifier for the chunk."
|
|
87
|
+
)
|
|
88
|
+
document_id: str = Field(
|
|
89
|
+
..., description="The identifier of the parent document."
|
|
90
|
+
)
|
|
91
|
+
vector: list[float] | None = Field(
|
|
92
|
+
None, description="Optional vector embedding for the chunk."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RAGSearchResult(StrictBaseModel):
|
|
97
|
+
"""A standard, built-in representation of a search result from a RAG vector search."""
|
|
98
|
+
|
|
99
|
+
chunk: RAGChunk = Field(
|
|
100
|
+
..., description="The RAG chunk returned as a search result."
|
|
101
|
+
)
|
|
102
|
+
score: float = Field(
|
|
103
|
+
...,
|
|
104
|
+
description="The similarity score of the chunk with respect to the query.",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AggregateStats(StrictBaseModel):
|
|
109
|
+
"""A standard, built-in representation of aggregate statistics."""
|
|
110
|
+
|
|
111
|
+
num_successful: int = Field(
|
|
112
|
+
..., description="The count of successful messages processed."
|
|
113
|
+
)
|
|
114
|
+
num_failed: int = Field(
|
|
115
|
+
..., description="The count of failed messages processed."
|
|
116
|
+
)
|
|
117
|
+
num_total: int = Field(
|
|
118
|
+
..., description="The total count of messages processed."
|
|
119
|
+
)
|