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
@@ -0,0 +1,171 @@
1
+ from abc import abstractmethod
2
+ from typing import Any, AsyncIterator
3
+
4
+ from aiostream import stream
5
+ from opentelemetry.trace import Status, StatusCode
6
+
7
+ from qtype.interpreter.base.base_step_executor import StepExecutor
8
+ from qtype.interpreter.base.executor_context import ExecutorContext
9
+ from qtype.interpreter.types import FlowMessage
10
+
11
+
12
+ class BatchedStepExecutor(StepExecutor):
13
+ """
14
+ Executor for steps that benefit from API-level batching.
15
+
16
+ This executor groups messages into batches and processes them together,
17
+ which is useful for operations that can leverage batch APIs for better
18
+ performance (e.g., GPU operations, bulk database operations, batch inference).
19
+
20
+ Like StepExecutor, this supports concurrent processing, but the unit of
21
+ concurrency is the batch rather than individual messages.
22
+
23
+ **Subclass Requirements:**
24
+ - Must implement `process_batch()` to handle batch processing
25
+ - Must NOT implement `process_message()` (it's handled automatically)
26
+ - Can optionally implement `finalize()` for cleanup/terminal operations
27
+
28
+ Args:
29
+ step: The semantic step model defining behavior and configuration
30
+ on_stream_event: Optional callback for real-time streaming events
31
+ on_progress: Optional callback for progress updates
32
+ **dependencies: Executor-specific dependencies
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ step: Any,
38
+ context: ExecutorContext,
39
+ **dependencies: Any,
40
+ ):
41
+ super().__init__(step, context, **dependencies)
42
+ # Override the processor to use batch processing with telemetry
43
+ # instead of message processing
44
+ self._processor = self._process_batch_with_telemetry
45
+
46
+ def _prepare_message_stream(
47
+ self, valid_messages: AsyncIterator[FlowMessage]
48
+ ) -> Any:
49
+ """
50
+ Prepare messages by chunking them into batches.
51
+
52
+ Overrides the base implementation to group messages into batches
53
+ based on the step's batch_config.
54
+
55
+ Args:
56
+ valid_messages: Stream of valid (non-failed) messages
57
+
58
+ Returns:
59
+ Stream of message batches (AsyncIterable[list[FlowMessage]])
60
+ """
61
+ # Determine batch size from step configuration
62
+ batch_size = 1
63
+ if (
64
+ hasattr(self.step, "batch_config")
65
+ and self.step.batch_config is not None # type: ignore[attr-defined]
66
+ ):
67
+ batch_size = self.step.batch_config.batch_size # type: ignore[attr-defined]
68
+
69
+ # Group messages into batches
70
+ return stream.chunks(valid_messages, batch_size)
71
+
72
+ async def process_message(
73
+ self, message: FlowMessage
74
+ ) -> AsyncIterator[FlowMessage]:
75
+ """
76
+ Process a single message by wrapping it in a batch of one.
77
+
78
+ This method is implemented automatically to satisfy the base class
79
+ contract. Subclasses should NOT override this method.
80
+
81
+ Args:
82
+ message: The input message to process
83
+
84
+ Yields:
85
+ Processed messages from the batch
86
+ """
87
+ raise NotImplementedError(
88
+ "Batch executors should call process_batch, not process_message."
89
+ )
90
+ yield # type: ignore[misc]
91
+
92
+ @abstractmethod
93
+ async def process_batch(
94
+ self, batch: list[FlowMessage]
95
+ ) -> AsyncIterator[FlowMessage]:
96
+ """
97
+ Process a batch of messages as a single API call.
98
+
99
+ Subclasses MUST implement this method to define how batches are
100
+ processed together for improved performance.
101
+
102
+ This is a many-to-many operation: a batch of input messages yields
103
+ a corresponding set of output messages. Messages should be yielded
104
+ as they become available to maintain memory efficiency (don't collect
105
+ all results before yielding).
106
+
107
+ This method is automatically wrapped with telemetry tracing when
108
+ called through the executor's execution pipeline.
109
+
110
+ Args:
111
+ batch: List of input messages to process as a batch
112
+
113
+ Yields:
114
+ Processed messages corresponding to the input batch
115
+ """
116
+ yield # type: ignore[misc]
117
+
118
+ async def _process_batch_with_telemetry(
119
+ self, batch: list[FlowMessage]
120
+ ) -> AsyncIterator[FlowMessage]:
121
+ """
122
+ Internal wrapper that adds telemetry tracing to process_batch.
123
+
124
+ This method creates a span for batch processing operations,
125
+ automatically recording batch size, errors, and success metrics.
126
+ """
127
+ span = self._tracer.start_span(
128
+ f"step.{self.step.id}.process_batch",
129
+ attributes={
130
+ "batch.size": len(batch),
131
+ },
132
+ )
133
+
134
+ try:
135
+ output_count = 0
136
+ error_count = 0
137
+
138
+ async for output_msg in self.process_batch(batch):
139
+ output_count += 1
140
+ if output_msg.is_failed():
141
+ error_count += 1
142
+ span.add_event(
143
+ "message_failed",
144
+ {
145
+ "error": str(output_msg.error),
146
+ },
147
+ )
148
+ yield output_msg
149
+
150
+ # Record processing metrics
151
+ span.set_attribute("batch.outputs", output_count)
152
+ span.set_attribute("batch.errors", error_count)
153
+
154
+ if error_count > 0:
155
+ span.set_status(
156
+ Status(
157
+ StatusCode.ERROR,
158
+ f"{error_count} of {output_count} messages failed",
159
+ )
160
+ )
161
+ else:
162
+ span.set_status(Status(StatusCode.OK))
163
+
164
+ except Exception as e:
165
+ span.record_exception(e)
166
+ span.set_status(
167
+ Status(StatusCode.ERROR, f"Batch processing failed: {e}")
168
+ )
169
+ raise
170
+ finally:
171
+ span.end()
@@ -0,0 +1,50 @@
1
+ """
2
+ Custom exception types for the QType interpreter.
3
+
4
+ This module provides specialized exception classes for better error handling
5
+ and reporting throughout the interpreter layer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class SecretResolutionError(Exception):
12
+ """
13
+ Raised when a secret cannot be resolved.
14
+
15
+ This exception is raised when attempting to resolve a SecretReference
16
+ but the operation fails due to:
17
+ - No secret manager configured
18
+ - Secret not found in the secret store
19
+ - Invalid secret format or structure
20
+ - Authentication/authorization failures
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ secret_name: str,
26
+ context: str = "",
27
+ cause: Exception | None = None,
28
+ ):
29
+ """
30
+ Initialize SecretResolutionError with structured attributes.
31
+
32
+ Args:
33
+ secret_name: Name/ID/ARN of the secret that failed to resolve
34
+ context: Optional context describing where resolution failed
35
+ (e.g., "auth provider 'my_auth'", "step 'my_step'")
36
+ cause: Optional underlying exception that caused the failure
37
+ """
38
+ self.secret_name = secret_name
39
+ self.context = context
40
+ self.cause = cause
41
+ super().__init__(self._format_message())
42
+
43
+ def _format_message(self) -> str:
44
+ """Format the error message from structured attributes."""
45
+ msg = f"Failed to resolve secret '{self.secret_name}'"
46
+ if self.context:
47
+ msg += f" in {self.context}"
48
+ if self.cause:
49
+ msg += f": {self.cause}"
50
+ return msg
@@ -0,0 +1,91 @@
1
+ """
2
+ Execution context for flow and step executors.
3
+
4
+ This module provides the ExecutorContext dataclass that bundles cross-cutting
5
+ concerns threaded through the execution pipeline.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from dataclasses import dataclass, field
12
+
13
+ from opentelemetry.trace import Tracer
14
+
15
+ from qtype.interpreter.base.secrets import SecretManagerBase
16
+ from qtype.interpreter.types import ProgressCallback, StreamingCallback
17
+
18
+
19
+ @dataclass
20
+ class ExecutorContext:
21
+ """
22
+ Runtime context for flow execution shared across all executors.
23
+
24
+ This bundles cross-cutting concerns that need to be threaded through
25
+ the execution pipeline but aren't specific to individual step types.
26
+ Using a context object reduces parameter threading boilerplate while
27
+ keeping dependencies explicit and testable.
28
+
29
+ Secret Resolution Lifecycle:
30
+ Secrets are resolved EARLY in the execution pipeline, following a
31
+ fail-fast principle:
32
+
33
+ 1. At executor construction time, any SecretReferences in step
34
+ configuration are resolved
35
+ 2. At auth context creation time, SecretReferences in auth providers
36
+ are resolved (via auth() context manager)
37
+ 3. Resolution failures raise SecretResolutionError immediately,
38
+ preventing execution from starting with invalid configuration
39
+
40
+ This ensures:
41
+ - Errors are caught before expensive operations begin
42
+ - All secrets are validated at initialization
43
+ - No partial execution with missing secrets
44
+ - Clear, actionable error messages at startup
45
+
46
+ Attributes:
47
+ secret_manager: Secret manager for resolving SecretReferences at
48
+ runtime. Always present (uses NoOpSecretManager if no secrets
49
+ are configured), eliminating the need for None checks.
50
+ on_stream_event: Optional callback for streaming real-time execution
51
+ events (chunks, steps, errors) to clients.
52
+ on_progress: Optional callback for progress updates during execution.
53
+ tracer: OpenTelemetry tracer for distributed tracing and observability.
54
+ Defaults to a no-op tracer if telemetry is not configured.
55
+ thread_pool: Shared thread pool for running synchronous operations
56
+ in async contexts. Defaults to a pool with 100 threads to support
57
+ high concurrency workloads without thread exhaustion.
58
+
59
+ Example:
60
+ ```python
61
+ from qtype.interpreter.base.executor_context import ExecutorContext
62
+ from qtype.interpreter.base.secrets import create_secret_manager
63
+ from opentelemetry import trace
64
+
65
+ context = ExecutorContext(
66
+ secret_manager=create_secret_manager(config),
67
+ on_stream_event=my_stream_callback,
68
+ tracer=trace.get_tracer(__name__)
69
+ )
70
+
71
+ executor = create_executor(step, context=context)
72
+ ```
73
+ """
74
+
75
+ secret_manager: SecretManagerBase
76
+ on_stream_event: StreamingCallback | None = None
77
+ on_progress: ProgressCallback | None = None
78
+ tracer: Tracer | None = None
79
+ thread_pool: ThreadPoolExecutor = field(
80
+ default_factory=lambda: ThreadPoolExecutor(max_workers=100)
81
+ )
82
+
83
+ def cleanup(self) -> None:
84
+ """
85
+ Clean up resources held by the context.
86
+
87
+ This should be called when the context is no longer needed to ensure
88
+ proper cleanup of the thread pool and any other resources.
89
+ """
90
+ if self.thread_pool:
91
+ self.thread_pool.shutdown(wait=True)
@@ -0,0 +1,84 @@
1
+ from qtype.semantic.model import (
2
+ Agent,
3
+ Aggregate,
4
+ BedrockReranker,
5
+ Decoder,
6
+ DocToTextConverter,
7
+ DocumentEmbedder,
8
+ DocumentSearch,
9
+ DocumentSource,
10
+ DocumentSplitter,
11
+ Echo,
12
+ FieldExtractor,
13
+ FileSource,
14
+ FileWriter,
15
+ IndexUpsert,
16
+ InvokeEmbedding,
17
+ InvokeFlow,
18
+ InvokeTool,
19
+ LLMInference,
20
+ PromptTemplate,
21
+ SQLSource,
22
+ Step,
23
+ VectorSearch,
24
+ )
25
+
26
+ from .batch_step_executor import StepExecutor
27
+ from .executor_context import ExecutorContext
28
+
29
+ # Lazy-load executor classes only when needed
30
+ # This avoids importing heavy dependencies until actually required
31
+ EXECUTOR_REGISTRY = {
32
+ Agent: "qtype.interpreter.executors.agent_executor.AgentExecutor",
33
+ Aggregate: "qtype.interpreter.executors.aggregate_executor.AggregateExecutor",
34
+ BedrockReranker: "qtype.interpreter.executors.bedrock_reranker_executor.BedrockRerankerExecutor",
35
+ Decoder: "qtype.interpreter.executors.decoder_executor.DecoderExecutor",
36
+ DocToTextConverter: "qtype.interpreter.executors.doc_to_text_executor.DocToTextConverterExecutor",
37
+ DocumentEmbedder: "qtype.interpreter.executors.document_embedder_executor.DocumentEmbedderExecutor",
38
+ DocumentSearch: "qtype.interpreter.executors.document_search_executor.DocumentSearchExecutor",
39
+ DocumentSource: "qtype.interpreter.executors.document_source_executor.DocumentSourceExecutor",
40
+ DocumentSplitter: "qtype.interpreter.executors.document_splitter_executor.DocumentSplitterExecutor",
41
+ Echo: "qtype.interpreter.executors.echo_executor.EchoExecutor",
42
+ FieldExtractor: "qtype.interpreter.executors.field_extractor_executor.FieldExtractorExecutor",
43
+ FileSource: "qtype.interpreter.executors.file_source_executor.FileSourceExecutor",
44
+ FileWriter: "qtype.interpreter.executors.file_writer_executor.FileWriterExecutor",
45
+ IndexUpsert: "qtype.interpreter.executors.index_upsert_executor.IndexUpsertExecutor",
46
+ InvokeEmbedding: "qtype.interpreter.executors.invoke_embedding_executor.InvokeEmbeddingExecutor",
47
+ InvokeFlow: "qtype.interpreter.executors.invoke_flow_executor.InvokeFlowExecutor",
48
+ InvokeTool: "qtype.interpreter.executors.invoke_tool_executor.InvokeToolExecutor",
49
+ LLMInference: "qtype.interpreter.executors.llm_inference_executor.LLMInferenceExecutor",
50
+ PromptTemplate: "qtype.interpreter.executors.prompt_template_executor.PromptTemplateExecutor",
51
+ SQLSource: "qtype.interpreter.executors.sql_source_executor.SQLSourceExecutor",
52
+ VectorSearch: "qtype.interpreter.executors.vector_search_executor.VectorSearchExecutor",
53
+ }
54
+
55
+
56
+ def create_executor(
57
+ step: Step, context: ExecutorContext, **dependencies
58
+ ) -> StepExecutor:
59
+ """
60
+ Factory to create the appropriate executor for a given step.
61
+
62
+ Args:
63
+ step: The step to create an executor for
64
+ context: ExecutorContext containing cross-cutting concerns
65
+ **dependencies: Executor-specific dependencies
66
+
67
+ Returns:
68
+ StepExecutor: Configured executor instance
69
+ """
70
+ executor_path = EXECUTOR_REGISTRY.get(type(step))
71
+ if not executor_path:
72
+ raise ValueError(
73
+ f"No executor found for step type: {type(step).__name__}"
74
+ )
75
+
76
+ # Lazy-load the executor class
77
+ module_path, class_name = executor_path.rsplit(".", 1)
78
+ import importlib
79
+
80
+ module = importlib.import_module(module_path)
81
+ executor_class = getattr(module, class_name)
82
+
83
+ # This assumes the constructor takes the step, context, then dependencies
84
+ return executor_class(step, context, **dependencies)
@@ -0,0 +1,110 @@
1
+ from qtype.interpreter.types import FlowMessage, ProgressCallback
2
+
3
+
4
+ class ProgressTracker:
5
+ """
6
+ Tracks progress for step execution.
7
+
8
+ This class encapsulates all progress tracking logic, separating it from
9
+ the execution logic in StepExecutor.
10
+
11
+ Attributes:
12
+ step_id: ID of the step being tracked
13
+ items_processed: Total number of items processed
14
+ items_in_error: Number of items that encountered errors
15
+ total_items: Total expected items (None if unknown)
16
+ """
17
+
18
+ def __init__(self, step_id: str, total_items: int | None = None):
19
+ self.step_id = step_id
20
+ self.items_processed = 0
21
+ self.items_in_error = 0
22
+ self.total_items = total_items
23
+ self.cache_hits = None
24
+ self.cache_misses = None
25
+
26
+ @property
27
+ def items_succeeded(self) -> int:
28
+ """
29
+ Number of items successfully processed.
30
+
31
+ This is derived from items_processed and items_in_error to avoid
32
+ state inconsistency.
33
+ """
34
+ return self.items_processed - self.items_in_error
35
+
36
+ def update(
37
+ self,
38
+ on_progress: ProgressCallback | None,
39
+ processed_delta: int,
40
+ error_delta: int,
41
+ hit_delta: int | None = None,
42
+ miss_delta: int | None = None,
43
+ ) -> None:
44
+ """
45
+ Update progress counters and invoke the progress callback.
46
+
47
+ Internal state is always updated regardless of whether a callback
48
+ is provided. This ensures the aggregator can access accurate counts.
49
+
50
+ Args:
51
+ on_progress: Optional callback to notify of progress updates
52
+ processed_delta: Number of items processed in this update
53
+ error_delta: Number of items that failed in this update
54
+ """
55
+ self.items_processed += processed_delta
56
+ self.items_in_error += error_delta
57
+
58
+ if hit_delta is not None:
59
+ self.cache_hits = (
60
+ self.cache_hits + hit_delta
61
+ if self.cache_hits is not None
62
+ else hit_delta
63
+ )
64
+ if miss_delta is not None:
65
+ self.cache_misses = (
66
+ self.cache_misses + miss_delta
67
+ if self.cache_misses is not None
68
+ else miss_delta
69
+ )
70
+
71
+ if on_progress:
72
+ on_progress(
73
+ self.step_id,
74
+ self.items_processed,
75
+ self.items_in_error,
76
+ self.items_succeeded,
77
+ self.total_items,
78
+ self.cache_hits,
79
+ self.cache_misses,
80
+ )
81
+
82
+ def update_for_message(
83
+ self,
84
+ message: FlowMessage,
85
+ on_progress: ProgressCallback | None,
86
+ ) -> None:
87
+ """
88
+ Update progress based on a single message result.
89
+
90
+ Args:
91
+ message: The message to check for success/failure
92
+ on_progress: Optional callback to notify of progress updates
93
+ """
94
+ self.update(on_progress, 1, 1 if message.is_failed() else 0)
95
+
96
+ def increment_cache(
97
+ self,
98
+ on_progress: ProgressCallback | None,
99
+ hit_delta: int = 0,
100
+ miss_delta: int = 0,
101
+ ) -> None:
102
+ """
103
+ Increment cache hit/miss counters.
104
+
105
+ Args:
106
+ on_progress: Optional callback to notify of progress updates
107
+ hit_delta: Number of cache hits to add
108
+ miss_delta: Number of cache misses to add
109
+ """
110
+ self.update(on_progress, 0, 0, hit_delta, miss_delta)