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.
Files changed (137) hide show
  1. qtype/application/commons/tools.py +1 -1
  2. qtype/application/converters/tools_from_api.py +476 -11
  3. qtype/application/converters/tools_from_module.py +38 -14
  4. qtype/application/converters/types.py +15 -30
  5. qtype/application/documentation.py +1 -1
  6. qtype/application/facade.py +102 -85
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +5 -1
  9. qtype/commands/convert.py +52 -6
  10. qtype/commands/generate.py +44 -4
  11. qtype/commands/run.py +78 -36
  12. qtype/commands/serve.py +74 -44
  13. qtype/commands/validate.py +37 -14
  14. qtype/commands/visualize.py +46 -25
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +86 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +753 -264
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +63 -136
  24. qtype/interpreter/auth/aws.py +19 -9
  25. qtype/interpreter/auth/generic.py +93 -16
  26. qtype/interpreter/base/base_step_executor.py +436 -0
  27. qtype/interpreter/base/batch_step_executor.py +171 -0
  28. qtype/interpreter/base/exceptions.py +50 -0
  29. qtype/interpreter/base/executor_context.py +91 -0
  30. qtype/interpreter/base/factory.py +84 -0
  31. qtype/interpreter/base/progress_tracker.py +110 -0
  32. qtype/interpreter/base/secrets.py +339 -0
  33. qtype/interpreter/base/step_cache.py +74 -0
  34. qtype/interpreter/base/stream_emitter.py +469 -0
  35. qtype/interpreter/conversions.py +495 -24
  36. qtype/interpreter/converters.py +79 -0
  37. qtype/interpreter/endpoints.py +355 -0
  38. qtype/interpreter/executors/agent_executor.py +242 -0
  39. qtype/interpreter/executors/aggregate_executor.py +93 -0
  40. qtype/interpreter/executors/bedrock_reranker_executor.py +195 -0
  41. qtype/interpreter/executors/decoder_executor.py +163 -0
  42. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  43. qtype/interpreter/executors/document_embedder_executor.py +123 -0
  44. qtype/interpreter/executors/document_search_executor.py +113 -0
  45. qtype/interpreter/executors/document_source_executor.py +118 -0
  46. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  47. qtype/interpreter/executors/echo_executor.py +63 -0
  48. qtype/interpreter/executors/field_extractor_executor.py +165 -0
  49. qtype/interpreter/executors/file_source_executor.py +101 -0
  50. qtype/interpreter/executors/file_writer_executor.py +110 -0
  51. qtype/interpreter/executors/index_upsert_executor.py +232 -0
  52. qtype/interpreter/executors/invoke_embedding_executor.py +104 -0
  53. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  54. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  55. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  56. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  57. qtype/interpreter/executors/sql_source_executor.py +106 -0
  58. qtype/interpreter/executors/vector_search_executor.py +91 -0
  59. qtype/interpreter/flow.py +172 -22
  60. qtype/interpreter/logging_progress.py +61 -0
  61. qtype/interpreter/metadata_api.py +115 -0
  62. qtype/interpreter/resource_cache.py +5 -4
  63. qtype/interpreter/rich_progress.py +225 -0
  64. qtype/interpreter/stream/chat/__init__.py +15 -0
  65. qtype/interpreter/stream/chat/converter.py +391 -0
  66. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  67. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  68. qtype/interpreter/stream/chat/vercel.py +609 -0
  69. qtype/interpreter/stream/utils/__init__.py +15 -0
  70. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  71. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  72. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  73. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  74. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  75. qtype/interpreter/telemetry.py +135 -8
  76. qtype/interpreter/tools/__init__.py +5 -0
  77. qtype/interpreter/tools/function_tool_helper.py +265 -0
  78. qtype/interpreter/types.py +330 -0
  79. qtype/interpreter/typing.py +83 -89
  80. qtype/interpreter/ui/404/index.html +1 -1
  81. qtype/interpreter/ui/404.html +1 -1
  82. qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  83. qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
  84. qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
  85. qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
  86. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  87. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  88. qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
  89. qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
  90. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  91. qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  92. qtype/interpreter/ui/icon.png +0 -0
  93. qtype/interpreter/ui/index.html +1 -1
  94. qtype/interpreter/ui/index.txt +5 -5
  95. qtype/semantic/checker.py +643 -0
  96. qtype/semantic/generate.py +268 -85
  97. qtype/semantic/loader.py +95 -0
  98. qtype/semantic/model.py +535 -163
  99. qtype/semantic/resolver.py +63 -19
  100. qtype/semantic/visualize.py +50 -35
  101. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/METADATA +22 -5
  102. qtype-0.1.7.dist-info/RECORD +137 -0
  103. qtype/dsl/base_types.py +0 -38
  104. qtype/dsl/validator.py +0 -464
  105. qtype/interpreter/batch/__init__.py +0 -0
  106. qtype/interpreter/batch/flow.py +0 -95
  107. qtype/interpreter/batch/sql_source.py +0 -95
  108. qtype/interpreter/batch/step.py +0 -63
  109. qtype/interpreter/batch/types.py +0 -41
  110. qtype/interpreter/batch/utils.py +0 -179
  111. qtype/interpreter/chat/chat_api.py +0 -237
  112. qtype/interpreter/chat/vercel.py +0 -314
  113. qtype/interpreter/exceptions.py +0 -10
  114. qtype/interpreter/step.py +0 -67
  115. qtype/interpreter/steps/__init__.py +0 -0
  116. qtype/interpreter/steps/agent.py +0 -114
  117. qtype/interpreter/steps/condition.py +0 -36
  118. qtype/interpreter/steps/decoder.py +0 -88
  119. qtype/interpreter/steps/llm_inference.py +0 -150
  120. qtype/interpreter/steps/prompt_template.py +0 -54
  121. qtype/interpreter/steps/search.py +0 -24
  122. qtype/interpreter/steps/tool.py +0 -53
  123. qtype/interpreter/streaming_helpers.py +0 -123
  124. qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
  125. qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
  126. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  127. qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
  128. qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
  129. qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
  130. qtype/interpreter/ui/favicon.ico +0 -0
  131. qtype/loader.py +0 -389
  132. qtype-0.0.12.dist-info/RECORD +0 -105
  133. /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  134. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/WHEEL +0 -0
  135. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
  136. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
  137. {qtype-0.0.12.dist-info → qtype-0.1.7.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[:30]: # 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) > 30:
161
+ error_msg += f" ... and {len(relevant_errors) - 30} 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,22 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
3
+ import asyncio
4
+ from contextlib import asynccontextmanager
4
5
  from pathlib import Path
5
6
 
6
- import pandas as pd
7
- from fastapi import FastAPI, HTTPException, Query
7
+ from fastapi import FastAPI
8
8
  from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import RedirectResponse
9
10
  from fastapi.staticfiles import StaticFiles
11
+ from opentelemetry import trace
10
12
 
11
- from qtype.dsl.base_types import StepCardinality
12
- from qtype.interpreter.batch.flow import batch_execute_flow
13
- from qtype.interpreter.batch.types import BatchConfig, ErrorMode
14
- from qtype.interpreter.flow import execute_flow
15
- from qtype.interpreter.typing import (
16
- create_input_type_model,
17
- 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,
18
18
  )
19
- from qtype.semantic.model import Application, Flow
19
+ from qtype.interpreter.metadata_api import create_metadata_endpoints
20
+ from qtype.semantic.model import Application
20
21
 
21
22
 
22
23
  class APIExecutor:
@@ -37,6 +38,7 @@ class APIExecutor:
37
38
  name: str | None = None,
38
39
  ui_enabled: bool = True,
39
40
  fast_api_args: dict | None = None,
41
+ servers: list[dict] | None = None,
40
42
  ) -> FastAPI:
41
43
  """Create FastAPI app with dynamic endpoints."""
42
44
  if fast_api_args is None:
@@ -45,7 +47,40 @@ class APIExecutor:
45
47
  "redoc_url": "/redoc",
46
48
  }
47
49
 
48
- app = FastAPI(title=name or "QType API", **fast_api_args)
50
+ # Add servers to FastAPI kwargs if provided
51
+ if servers is not None:
52
+ fast_api_args["servers"] = servers
53
+
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
+ )
49
84
 
50
85
  # Serve static UI files if they exist
51
86
  if ui_enabled:
@@ -65,132 +100,24 @@ class APIExecutor:
65
100
  StaticFiles(directory=str(ui_dir), html=True),
66
101
  name="ui",
67
102
  )
103
+ app.get("/", include_in_schema=False)(
104
+ lambda: RedirectResponse(url="/ui")
105
+ )
68
106
 
69
- flows = self.definition.flows if self.definition.flows else []
107
+ # Create metadata endpoints for flow discovery
108
+ create_metadata_endpoints(app, self.definition)
70
109
 
71
- # Dynamically generate POST endpoints for each flow
72
- for flow in flows:
73
- if flow.mode == "Chat":
74
- # For chat, we can create a single endpoint
75
- # that handles the chat interactions
76
- from qtype.interpreter.chat.chat_api import (
77
- create_chat_flow_endpoint,
78
- )
110
+ # Create executor context
111
+ context = ExecutorContext(
112
+ secret_manager=secret_manager,
113
+ tracer=trace.get_tracer(__name__),
114
+ )
79
115
 
80
- create_chat_flow_endpoint(app, flow)
81
- else:
82
- self._create_flow_endpoint(app, flow)
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)
83
122
 
84
123
  return app
85
-
86
- def _create_flow_endpoint(self, app: FastAPI, flow: Flow) -> None:
87
- """Create a dynamic POST endpoint for a specific flow."""
88
- flow_id = flow.id
89
-
90
- # determine if this is a batch inference
91
- is_batch = flow.cardinality == StepCardinality.many
92
-
93
- # Create dynamic request and response models for this flow
94
- RequestModel = create_input_type_model(flow, is_batch)
95
- ResponseModel = create_output_type_model(flow, is_batch)
96
-
97
- # Create the endpoint function with proper model binding
98
- if is_batch:
99
-
100
- def execute_flow_endpoint( # type: ignore
101
- request: RequestModel, # type: ignore
102
- error_mode: ErrorMode = Query(
103
- default=ErrorMode.FAIL,
104
- description="Error handling mode for batch processing",
105
- ),
106
- ) -> ResponseModel: # type: ignore
107
- try:
108
- # Make a copy of the flow to avoid modifying the original
109
- # TODO: Use session to ensure memory is not used across requests.
110
- flow_copy = flow.model_copy(deep=True)
111
- # convert the inputs into a dataframe with a single row
112
- inputs = pd.DataFrame(
113
- [i.model_dump() for i in request.inputs] # type: ignore
114
- )
115
-
116
- # Execute the flow
117
- results, errors = batch_execute_flow(
118
- flow_copy,
119
- inputs,
120
- batch_config=BatchConfig(error_mode=error_mode),
121
- )
122
-
123
- response_data = {
124
- "flow_id": flow_id,
125
- "outputs": results.to_dict(orient="records"),
126
- "errors": errors.to_dict(orient="records"),
127
- "num_results": len(results),
128
- "num_errors": len(errors),
129
- "num_inputs": len(inputs),
130
- "status": "success" if len(errors) == 0 else "partial",
131
- }
132
-
133
- # Return the response using the dynamic model
134
- return ResponseModel(**response_data) # type: ignore
135
-
136
- except Exception as e:
137
- logging.error("Batch Flow Execution Failed", exc_info=e)
138
- raise HTTPException(
139
- status_code=500,
140
- detail=f"Batch flow execution failed: {str(e)}",
141
- )
142
- else:
143
-
144
- def execute_flow_endpoint(request: RequestModel) -> ResponseModel: # type: ignore
145
- try:
146
- # Make a copy of the flow to avoid modifying the original
147
- # TODO: Use session to ensure memory is not used across requests.
148
- flow_copy = flow.model_copy(deep=True)
149
- # Set input values on the flow variables
150
- if flow_copy.inputs:
151
- for var in flow_copy.inputs:
152
- # Get the value from the request using the variable ID
153
- request_dict = request.model_dump() # type: ignore
154
- if var.id in request_dict:
155
- var.value = getattr(request, var.id)
156
- elif not var.is_set():
157
- raise HTTPException(
158
- status_code=400,
159
- detail=f"Required input '{var.id}' not provided",
160
- )
161
- return flow_copy
162
- # Execute the flow
163
- result_vars = execute_flow(flow_copy)
164
-
165
- # Extract output values
166
- outputs = {var.id: var.value for var in result_vars}
167
-
168
- response_data = {
169
- "flow_id": flow_id,
170
- "outputs": outputs,
171
- "status": "success",
172
- }
173
-
174
- # Return the response using the dynamic model
175
- return ResponseModel(**response_data) # type: ignore
176
-
177
- except Exception as e:
178
- raise HTTPException(
179
- status_code=500,
180
- detail=f"Flow execution failed: {str(e)}",
181
- )
182
-
183
- # Set the function annotations properly for FastAPI
184
- execute_flow_endpoint.__annotations__ = {
185
- "request": RequestModel,
186
- "error_mode": ErrorMode,
187
- "return": ResponseModel,
188
- }
189
-
190
- # Add the endpoint with explicit models
191
- app.post(
192
- f"/flows/{flow_id}",
193
- tags=["flow"],
194
- description=flow.description or "Execute a flow",
195
- response_model=ResponseModel,
196
- )(execute_flow_endpoint)
@@ -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(aws_provider: AWSAuthProvider) -> Generator[boto3.Session, None, None]:
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(aws_provider: AWSAuthProvider) -> boto3.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
- session_kwargs["aws_access_key_id"] = aws_provider.access_key_id
167
- session_kwargs["aws_secret_access_key"] = (
168
- aws_provider.secret_access_key
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
- session_kwargs["aws_session_token"] = aws_provider.session_token
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)