qtype 0.0.16__py3-none-any.whl → 0.1.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.
- 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 +94 -73
- qtype/base/types.py +227 -7
- qtype/cli.py +4 -0
- qtype/commands/convert.py +20 -8
- qtype/commands/generate.py +19 -27
- qtype/commands/run.py +73 -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 +57 -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 +74 -0
- qtype/interpreter/base/factory.py +117 -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 +462 -22
- qtype/interpreter/converters.py +77 -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 +107 -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 +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 +159 -22
- 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/{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 +63 -19
- qtype/semantic/visualize.py +28 -31
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/METADATA +16 -3
- qtype-0.1.1.dist-info/RECORD +135 -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.16.dist-info/RECORD +0 -106
- /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/WHEEL +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/top_level.txt +0 -0
qtype/dsl/parser.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse YAML dictionaries into DSL models.
|
|
3
|
+
|
|
4
|
+
This module handles the conversion of loaded YAML data into validated
|
|
5
|
+
Pydantic DSL models, including custom type extraction and building.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from qtype.base.types import CustomTypeRegistry
|
|
15
|
+
from qtype.dsl import model as dsl
|
|
16
|
+
from qtype.dsl.custom_types import build_dynamic_types
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_type_definitions(data: dict[str, Any] | list) -> list[dict]:
|
|
20
|
+
"""
|
|
21
|
+
Extract all custom type definitions from document.
|
|
22
|
+
|
|
23
|
+
Recursively finds type definitions in the document and any references.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
data: Parsed YAML dictionary or list
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of custom type definition dictionaries
|
|
30
|
+
"""
|
|
31
|
+
types = []
|
|
32
|
+
|
|
33
|
+
# Handle list documents (e.g., ToolList, ModelList)
|
|
34
|
+
if isinstance(data, list):
|
|
35
|
+
return types
|
|
36
|
+
|
|
37
|
+
# Add types from Application documents
|
|
38
|
+
if isinstance(data, dict):
|
|
39
|
+
types.extend(data.get("types", []))
|
|
40
|
+
|
|
41
|
+
# Handle TypeList documents (root is a list of types)
|
|
42
|
+
if "root" in data:
|
|
43
|
+
root = data["root"]
|
|
44
|
+
if (
|
|
45
|
+
isinstance(root, list)
|
|
46
|
+
and len(root) > 0
|
|
47
|
+
and "properties" in root[0]
|
|
48
|
+
):
|
|
49
|
+
types.extend(root)
|
|
50
|
+
|
|
51
|
+
# Recursively handle references
|
|
52
|
+
for ref in data.get("references", []):
|
|
53
|
+
types.extend(_extract_type_definitions(ref))
|
|
54
|
+
|
|
55
|
+
return types
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _simplify_field_path(loc: tuple) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Simplify a Pydantic error location path for readability.
|
|
61
|
+
|
|
62
|
+
Removes verbose union type names and formats array indices.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
loc: Error location tuple from Pydantic
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Simplified, readable field path string
|
|
69
|
+
"""
|
|
70
|
+
simplified = []
|
|
71
|
+
for part in loc:
|
|
72
|
+
part_str = str(part)
|
|
73
|
+
|
|
74
|
+
# Skip union type discriminator paths (too verbose)
|
|
75
|
+
if "tagged-union" in part_str or "Union[" in part_str:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Skip Reference wrapper types
|
|
79
|
+
if part_str.startswith("Reference["):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Format numeric indices as array indices
|
|
83
|
+
if isinstance(part, int):
|
|
84
|
+
simplified.append(f"[{part}]")
|
|
85
|
+
else:
|
|
86
|
+
simplified.append(part_str)
|
|
87
|
+
|
|
88
|
+
return " -> ".join(simplified).replace(" -> [", "[")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_relevant_error(error: dict | Any) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Determine if a validation error is relevant to show.
|
|
94
|
+
|
|
95
|
+
Filters out noise from union type matching attempts.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
error: Pydantic error dictionary
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if error should be shown to user
|
|
102
|
+
"""
|
|
103
|
+
loc_str = " -> ".join(str(loc) for loc in error["loc"])
|
|
104
|
+
error_type = error["type"]
|
|
105
|
+
|
|
106
|
+
# Filter out "should be a valid list" errors for document types
|
|
107
|
+
# These are just union matching attempts
|
|
108
|
+
if error_type == "list_type" and any(
|
|
109
|
+
doc_type in loc_str
|
|
110
|
+
for doc_type in [
|
|
111
|
+
"AuthorizationProviderList",
|
|
112
|
+
"ModelList",
|
|
113
|
+
"ToolList",
|
|
114
|
+
"TypeList",
|
|
115
|
+
"VariableList",
|
|
116
|
+
"AgentList",
|
|
117
|
+
"FlowList",
|
|
118
|
+
"IndexList",
|
|
119
|
+
]
|
|
120
|
+
):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Filter out Reference wrapper errors about $ref field
|
|
124
|
+
# These are duplicates of actual validation errors
|
|
125
|
+
if "Reference[" in loc_str and "$ref" in error["loc"][-1]:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _format_validation_errors(
|
|
132
|
+
validation_error: ValidationError, source_name: str | None
|
|
133
|
+
) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Format Pydantic validation errors in user-friendly way.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
validation_error: ValidationError from Pydantic
|
|
139
|
+
source_name: Optional source file name for context
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Formatted error message string
|
|
143
|
+
"""
|
|
144
|
+
# Filter and collect relevant errors
|
|
145
|
+
relevant_errors = [
|
|
146
|
+
error
|
|
147
|
+
for error in validation_error.errors()
|
|
148
|
+
if _is_relevant_error(error)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
if not relevant_errors:
|
|
152
|
+
# Fallback if all errors were filtered
|
|
153
|
+
error_msg = "Validation failed (see details above)"
|
|
154
|
+
else:
|
|
155
|
+
error_msg = "Validation failed:\n"
|
|
156
|
+
for error in relevant_errors[:5]: # Show max 5 errors
|
|
157
|
+
loc_path = _simplify_field_path(error["loc"])
|
|
158
|
+
error_msg += f" {loc_path}: {error['msg']}\n"
|
|
159
|
+
|
|
160
|
+
if len(relevant_errors) > 5:
|
|
161
|
+
error_msg += f" ... and {len(relevant_errors) - 5} more errors\n"
|
|
162
|
+
|
|
163
|
+
if source_name:
|
|
164
|
+
error_msg = f"In {source_name}:\n{error_msg}"
|
|
165
|
+
|
|
166
|
+
return error_msg
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_document(
|
|
170
|
+
yaml_data: dict[str, Any], source_name: str | None = None
|
|
171
|
+
) -> tuple[dsl.DocumentType, CustomTypeRegistry]:
|
|
172
|
+
"""
|
|
173
|
+
Parse validated YAML dictionary into DSL document.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
yaml_data: Pre-loaded YAML dictionary
|
|
177
|
+
source_name: Optional source name for error messages
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of (DocumentType, CustomTypeRegistry)
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: If validation fails
|
|
184
|
+
"""
|
|
185
|
+
# Extract and build custom types
|
|
186
|
+
type_defs = _extract_type_definitions(yaml_data)
|
|
187
|
+
custom_types = build_dynamic_types(type_defs)
|
|
188
|
+
|
|
189
|
+
# Validate with Pydantic
|
|
190
|
+
try:
|
|
191
|
+
document = dsl.Document.model_validate(
|
|
192
|
+
yaml_data, context={"custom_types": custom_types}
|
|
193
|
+
)
|
|
194
|
+
except ValidationError as e:
|
|
195
|
+
# Format validation errors nicely
|
|
196
|
+
error_msg = _format_validation_errors(e, source_name)
|
|
197
|
+
raise ValueError(error_msg) from e
|
|
198
|
+
|
|
199
|
+
# Extract root document from wrapper
|
|
200
|
+
return document.root, custom_types
|
qtype/dsl/types.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Utility types and functions for the DSL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime, time
|
|
6
|
+
|
|
7
|
+
from qtype.base.types import PrimitiveTypeEnum
|
|
8
|
+
|
|
9
|
+
# Mapping of QType primitive types to Python types for internal representations
|
|
10
|
+
PRIMITIVE_TO_PYTHON_TYPE = {
|
|
11
|
+
PrimitiveTypeEnum.audio: bytes,
|
|
12
|
+
PrimitiveTypeEnum.boolean: bool,
|
|
13
|
+
PrimitiveTypeEnum.bytes: bytes,
|
|
14
|
+
PrimitiveTypeEnum.date: date,
|
|
15
|
+
PrimitiveTypeEnum.datetime: datetime,
|
|
16
|
+
PrimitiveTypeEnum.int: int,
|
|
17
|
+
PrimitiveTypeEnum.file: bytes, # Use bytes for file content
|
|
18
|
+
PrimitiveTypeEnum.float: float,
|
|
19
|
+
PrimitiveTypeEnum.image: bytes, # Use bytes for image data
|
|
20
|
+
PrimitiveTypeEnum.text: str,
|
|
21
|
+
PrimitiveTypeEnum.time: time, # Use time for time representation
|
|
22
|
+
PrimitiveTypeEnum.video: bytes, # Use bytes for video data
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
PYTHON_TYPE_TO_PRIMITIVE_TYPE = {
|
|
26
|
+
bytes: PrimitiveTypeEnum.file,
|
|
27
|
+
bool: PrimitiveTypeEnum.boolean,
|
|
28
|
+
str: PrimitiveTypeEnum.text,
|
|
29
|
+
int: PrimitiveTypeEnum.int,
|
|
30
|
+
float: PrimitiveTypeEnum.float,
|
|
31
|
+
date: PrimitiveTypeEnum.date,
|
|
32
|
+
datetime: PrimitiveTypeEnum.datetime,
|
|
33
|
+
time: PrimitiveTypeEnum.time,
|
|
34
|
+
# TODO: decide on internal representation for images, video, and audio,
|
|
35
|
+
# or use annotation/hinting
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def python_type_for_list(element_type: PrimitiveTypeEnum) -> type:
|
|
40
|
+
"""
|
|
41
|
+
Get the Python list type for a given QType primitive element type.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
element_type: The primitive type of the list elements
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The corresponding Python list type (e.g., list[str] for text elements)
|
|
48
|
+
"""
|
|
49
|
+
element_python_type = PRIMITIVE_TO_PYTHON_TYPE[element_type]
|
|
50
|
+
return list[element_python_type]
|
qtype/interpreter/api.py
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
from fastapi import FastAPI, HTTPException, Query
|
|
7
|
+
from fastapi import FastAPI
|
|
8
8
|
from fastapi.middleware.cors import CORSMiddleware
|
|
9
9
|
from fastapi.responses import RedirectResponse
|
|
10
10
|
from fastapi.staticfiles import StaticFiles
|
|
11
|
+
from opentelemetry import trace
|
|
11
12
|
|
|
12
|
-
from qtype.
|
|
13
|
-
from qtype.interpreter.
|
|
14
|
-
from qtype.interpreter.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
create_input_type_model,
|
|
18
|
-
create_output_type_model,
|
|
13
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
14
|
+
from qtype.interpreter.base.secrets import create_secret_manager
|
|
15
|
+
from qtype.interpreter.endpoints import (
|
|
16
|
+
create_rest_endpoint,
|
|
17
|
+
create_streaming_endpoint,
|
|
19
18
|
)
|
|
20
|
-
from qtype.
|
|
19
|
+
from qtype.interpreter.metadata_api import create_metadata_endpoints
|
|
20
|
+
from qtype.semantic.model import Application
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class APIExecutor:
|
|
@@ -51,7 +51,36 @@ class APIExecutor:
|
|
|
51
51
|
if servers is not None:
|
|
52
52
|
fast_api_args["servers"] = servers
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Create secret manager if configured
|
|
55
|
+
secret_manager = create_secret_manager(self.definition.secret_manager)
|
|
56
|
+
|
|
57
|
+
# Create lifespan context manager for telemetry
|
|
58
|
+
@asynccontextmanager
|
|
59
|
+
async def lifespan(app: FastAPI):
|
|
60
|
+
"""Manage telemetry lifecycle during app startup/shutdown."""
|
|
61
|
+
tracer_provider = None
|
|
62
|
+
if self.definition.telemetry:
|
|
63
|
+
from qtype.interpreter.telemetry import register
|
|
64
|
+
|
|
65
|
+
tracer_provider = register(
|
|
66
|
+
self.definition.telemetry,
|
|
67
|
+
project_id=name or self.definition.id,
|
|
68
|
+
secret_manager=secret_manager,
|
|
69
|
+
)
|
|
70
|
+
yield
|
|
71
|
+
# Fire off telemetry shutdown in background for fast reloads
|
|
72
|
+
if tracer_provider is not None:
|
|
73
|
+
|
|
74
|
+
async def shutdown_telemetry():
|
|
75
|
+
tracer_provider.force_flush(timeout_millis=1000)
|
|
76
|
+
tracer_provider.shutdown()
|
|
77
|
+
|
|
78
|
+
asyncio.create_task(shutdown_telemetry())
|
|
79
|
+
|
|
80
|
+
# Create FastAPI app with lifespan
|
|
81
|
+
app = FastAPI(
|
|
82
|
+
title=name or "QType API", lifespan=lifespan, **fast_api_args
|
|
83
|
+
)
|
|
55
84
|
|
|
56
85
|
# Serve static UI files if they exist
|
|
57
86
|
if ui_enabled:
|
|
@@ -71,132 +100,24 @@ class APIExecutor:
|
|
|
71
100
|
StaticFiles(directory=str(ui_dir), html=True),
|
|
72
101
|
name="ui",
|
|
73
102
|
)
|
|
74
|
-
app.get("/"
|
|
103
|
+
app.get("/", include_in_schema=False)(
|
|
104
|
+
lambda: RedirectResponse(url="/ui")
|
|
105
|
+
)
|
|
75
106
|
|
|
76
|
-
|
|
107
|
+
# Create metadata endpoints for flow discovery
|
|
108
|
+
create_metadata_endpoints(app, self.definition)
|
|
77
109
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
from qtype.interpreter.chat.chat_api import (
|
|
84
|
-
create_chat_flow_endpoint,
|
|
85
|
-
)
|
|
110
|
+
# Create executor context
|
|
111
|
+
context = ExecutorContext(
|
|
112
|
+
secret_manager=secret_manager,
|
|
113
|
+
tracer=trace.get_tracer(__name__),
|
|
114
|
+
)
|
|
86
115
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
# Create unified invoke endpoints for each flow
|
|
117
|
+
flows = self.definition.flows if self.definition.flows else []
|
|
118
|
+
for flow in flows:
|
|
119
|
+
if flow.interface is not None:
|
|
120
|
+
create_streaming_endpoint(app, flow, context)
|
|
121
|
+
create_rest_endpoint(app, flow, context)
|
|
90
122
|
|
|
91
123
|
return app
|
|
92
|
-
|
|
93
|
-
def _create_flow_endpoint(self, app: FastAPI, flow: Flow) -> None:
|
|
94
|
-
"""Create a dynamic POST endpoint for a specific flow."""
|
|
95
|
-
flow_id = flow.id
|
|
96
|
-
|
|
97
|
-
# determine if this is a batch inference
|
|
98
|
-
is_batch = flow.cardinality == StepCardinality.many
|
|
99
|
-
|
|
100
|
-
# Create dynamic request and response models for this flow
|
|
101
|
-
RequestModel = create_input_type_model(flow, is_batch)
|
|
102
|
-
ResponseModel = create_output_type_model(flow, is_batch)
|
|
103
|
-
|
|
104
|
-
# Create the endpoint function with proper model binding
|
|
105
|
-
if is_batch:
|
|
106
|
-
|
|
107
|
-
def execute_flow_endpoint( # type: ignore
|
|
108
|
-
request: RequestModel, # type: ignore
|
|
109
|
-
error_mode: ErrorMode = Query(
|
|
110
|
-
default=ErrorMode.FAIL,
|
|
111
|
-
description="Error handling mode for batch processing",
|
|
112
|
-
),
|
|
113
|
-
) -> ResponseModel: # type: ignore
|
|
114
|
-
try:
|
|
115
|
-
# Make a copy of the flow to avoid modifying the original
|
|
116
|
-
# TODO: Use session to ensure memory is not used across requests.
|
|
117
|
-
flow_copy = flow.model_copy(deep=True)
|
|
118
|
-
# convert the inputs into a dataframe with a single row
|
|
119
|
-
inputs = pd.DataFrame(
|
|
120
|
-
[i.model_dump() for i in request.inputs] # type: ignore
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
# Execute the flow
|
|
124
|
-
results, errors = batch_execute_flow(
|
|
125
|
-
flow_copy,
|
|
126
|
-
inputs,
|
|
127
|
-
batch_config=BatchConfig(error_mode=error_mode),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
response_data = {
|
|
131
|
-
"flow_id": flow_id,
|
|
132
|
-
"outputs": results.to_dict(orient="records"),
|
|
133
|
-
"errors": errors.to_dict(orient="records"),
|
|
134
|
-
"num_results": len(results),
|
|
135
|
-
"num_errors": len(errors),
|
|
136
|
-
"num_inputs": len(inputs),
|
|
137
|
-
"status": "success" if len(errors) == 0 else "partial",
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# Return the response using the dynamic model
|
|
141
|
-
return ResponseModel(**response_data) # type: ignore
|
|
142
|
-
|
|
143
|
-
except Exception as e:
|
|
144
|
-
logging.error("Batch Flow Execution Failed", exc_info=e)
|
|
145
|
-
raise HTTPException(
|
|
146
|
-
status_code=500,
|
|
147
|
-
detail=f"Batch flow execution failed: {str(e)}",
|
|
148
|
-
)
|
|
149
|
-
else:
|
|
150
|
-
|
|
151
|
-
def execute_flow_endpoint(request: RequestModel) -> ResponseModel: # type: ignore
|
|
152
|
-
try:
|
|
153
|
-
# Make a copy of the flow to avoid modifying the original
|
|
154
|
-
# TODO: Use session to ensure memory is not used across requests.
|
|
155
|
-
flow_copy = flow.model_copy(deep=True)
|
|
156
|
-
# Set input values on the flow variables
|
|
157
|
-
if flow_copy.inputs:
|
|
158
|
-
for var in flow_copy.inputs:
|
|
159
|
-
# Get the value from the request using the variable ID
|
|
160
|
-
request_dict = request.model_dump() # type: ignore
|
|
161
|
-
if var.id in request_dict:
|
|
162
|
-
var.value = getattr(request, var.id)
|
|
163
|
-
elif not var.is_set():
|
|
164
|
-
raise HTTPException(
|
|
165
|
-
status_code=400,
|
|
166
|
-
detail=f"Required input '{var.id}' not provided",
|
|
167
|
-
)
|
|
168
|
-
# Execute the flow
|
|
169
|
-
result_vars = execute_flow(flow_copy)
|
|
170
|
-
|
|
171
|
-
# Extract output values
|
|
172
|
-
outputs = {var.id: var.value for var in result_vars}
|
|
173
|
-
|
|
174
|
-
response_data = {
|
|
175
|
-
"flow_id": flow_id,
|
|
176
|
-
"outputs": outputs,
|
|
177
|
-
"status": "success",
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
# Return the response using the dynamic model
|
|
181
|
-
return ResponseModel(**response_data) # type: ignore
|
|
182
|
-
|
|
183
|
-
except Exception as e:
|
|
184
|
-
raise HTTPException(
|
|
185
|
-
status_code=500,
|
|
186
|
-
detail=f"Flow execution failed: {str(e)}",
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
# Set the function annotations properly for FastAPI
|
|
190
|
-
execute_flow_endpoint.__annotations__ = {
|
|
191
|
-
"request": RequestModel,
|
|
192
|
-
"error_mode": ErrorMode,
|
|
193
|
-
"return": ResponseModel,
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# Add the endpoint with explicit models
|
|
197
|
-
app.post(
|
|
198
|
-
f"/flows/{flow_id}",
|
|
199
|
-
tags=["flow"],
|
|
200
|
-
description=flow.description or "Execute a flow",
|
|
201
|
-
response_model=ResponseModel,
|
|
202
|
-
)(execute_flow_endpoint)
|
qtype/interpreter/auth/aws.py
CHANGED
|
@@ -17,6 +17,7 @@ from botocore.exceptions import ( # type: ignore[import-untyped]
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from qtype.interpreter.auth.cache import cache_auth, get_cached_auth
|
|
20
|
+
from qtype.interpreter.base.secrets import SecretManagerBase
|
|
20
21
|
from qtype.semantic.model import AWSAuthProvider
|
|
21
22
|
|
|
22
23
|
|
|
@@ -56,7 +57,10 @@ def _is_session_valid(session: boto3.Session) -> bool:
|
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
@contextmanager
|
|
59
|
-
def aws(
|
|
60
|
+
def aws(
|
|
61
|
+
aws_provider: AWSAuthProvider,
|
|
62
|
+
secret_manager: SecretManagerBase,
|
|
63
|
+
) -> Generator[boto3.Session, None, None]:
|
|
60
64
|
"""
|
|
61
65
|
Create a boto3 Session using AWS authentication provider configuration.
|
|
62
66
|
|
|
@@ -113,7 +117,7 @@ def aws(aws_provider: AWSAuthProvider) -> Generator[boto3.Session, None, None]:
|
|
|
113
117
|
return
|
|
114
118
|
|
|
115
119
|
# Cache miss or invalid session - create new session
|
|
116
|
-
session = _create_session(aws_provider)
|
|
120
|
+
session = _create_session(aws_provider, secret_manager)
|
|
117
121
|
|
|
118
122
|
# Validate the session by attempting to get credentials
|
|
119
123
|
credentials = session.get_credentials()
|
|
@@ -137,12 +141,16 @@ def aws(aws_provider: AWSAuthProvider) -> Generator[boto3.Session, None, None]:
|
|
|
137
141
|
) from e
|
|
138
142
|
|
|
139
143
|
|
|
140
|
-
def _create_session(
|
|
144
|
+
def _create_session(
|
|
145
|
+
aws_provider: AWSAuthProvider,
|
|
146
|
+
secret_manager: SecretManagerBase,
|
|
147
|
+
) -> boto3.Session:
|
|
141
148
|
"""
|
|
142
149
|
Create a boto3 Session based on the AWS provider configuration.
|
|
143
150
|
|
|
144
151
|
Args:
|
|
145
152
|
aws_provider: AWSAuthProvider with authentication details
|
|
153
|
+
secret_manager: Secret manager for resolving SecretReferences
|
|
146
154
|
|
|
147
155
|
Returns:
|
|
148
156
|
boto3.Session: Configured session
|
|
@@ -162,14 +170,16 @@ def _create_session(aws_provider: AWSAuthProvider) -> boto3.Session:
|
|
|
162
170
|
session_kwargs["profile_name"] = aws_provider.profile_name
|
|
163
171
|
|
|
164
172
|
elif aws_provider.access_key_id and aws_provider.secret_access_key:
|
|
165
|
-
# Use direct credentials
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
# Use direct credentials - resolve secrets
|
|
174
|
+
context = f"AWS auth provider '{aws_provider.id}'"
|
|
175
|
+
access_key = secret_manager(aws_provider.access_key_id, context)
|
|
176
|
+
secret_key = secret_manager(aws_provider.secret_access_key, context)
|
|
177
|
+
session_kwargs["aws_access_key_id"] = access_key
|
|
178
|
+
session_kwargs["aws_secret_access_key"] = secret_key
|
|
170
179
|
|
|
171
180
|
if aws_provider.session_token:
|
|
172
|
-
|
|
181
|
+
session_token = secret_manager(aws_provider.session_token, context)
|
|
182
|
+
session_kwargs["aws_session_token"] = session_token
|
|
173
183
|
|
|
174
184
|
# Create the base session
|
|
175
185
|
session = boto3.Session(**session_kwargs)
|