qtype 0.0.12__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qtype/application/commons/tools.py +1 -1
- qtype/application/converters/tools_from_api.py +476 -11
- qtype/application/converters/tools_from_module.py +38 -14
- qtype/application/converters/types.py +15 -30
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +102 -85
- qtype/base/types.py +227 -7
- qtype/cli.py +5 -1
- qtype/commands/convert.py +52 -6
- qtype/commands/generate.py +44 -4
- qtype/commands/run.py +78 -36
- qtype/commands/serve.py +74 -44
- qtype/commands/validate.py +37 -14
- qtype/commands/visualize.py +46 -25
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +86 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +753 -264
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +63 -136
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +436 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +91 -0
- qtype/interpreter/base/factory.py +84 -0
- qtype/interpreter/base/progress_tracker.py +110 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +74 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +495 -24
- qtype/interpreter/converters.py +79 -0
- qtype/interpreter/endpoints.py +355 -0
- qtype/interpreter/executors/agent_executor.py +242 -0
- qtype/interpreter/executors/aggregate_executor.py +93 -0
- qtype/interpreter/executors/bedrock_reranker_executor.py +195 -0
- qtype/interpreter/executors/decoder_executor.py +163 -0
- qtype/interpreter/executors/doc_to_text_executor.py +112 -0
- qtype/interpreter/executors/document_embedder_executor.py +123 -0
- qtype/interpreter/executors/document_search_executor.py +113 -0
- qtype/interpreter/executors/document_source_executor.py +118 -0
- qtype/interpreter/executors/document_splitter_executor.py +105 -0
- qtype/interpreter/executors/echo_executor.py +63 -0
- qtype/interpreter/executors/field_extractor_executor.py +165 -0
- qtype/interpreter/executors/file_source_executor.py +101 -0
- qtype/interpreter/executors/file_writer_executor.py +110 -0
- qtype/interpreter/executors/index_upsert_executor.py +232 -0
- qtype/interpreter/executors/invoke_embedding_executor.py +104 -0
- qtype/interpreter/executors/invoke_flow_executor.py +51 -0
- qtype/interpreter/executors/invoke_tool_executor.py +358 -0
- qtype/interpreter/executors/llm_inference_executor.py +272 -0
- qtype/interpreter/executors/prompt_template_executor.py +78 -0
- qtype/interpreter/executors/sql_source_executor.py +106 -0
- qtype/interpreter/executors/vector_search_executor.py +91 -0
- qtype/interpreter/flow.py +172 -22
- qtype/interpreter/logging_progress.py +61 -0
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- qtype/interpreter/rich_progress.py +225 -0
- qtype/interpreter/stream/chat/__init__.py +15 -0
- qtype/interpreter/stream/chat/converter.py +391 -0
- qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
- qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
- qtype/interpreter/stream/chat/vercel.py +609 -0
- qtype/interpreter/stream/utils/__init__.py +15 -0
- qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
- qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
- qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
- qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
- qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
- qtype/interpreter/telemetry.py +135 -8
- qtype/interpreter/tools/__init__.py +5 -0
- qtype/interpreter/tools/function_tool_helper.py +265 -0
- qtype/interpreter/types.py +330 -0
- qtype/interpreter/typing.py +83 -89
- qtype/interpreter/ui/404/index.html +1 -1
- qtype/interpreter/ui/404.html +1 -1
- qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
- qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
- qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +5 -5
- qtype/semantic/checker.py +643 -0
- qtype/semantic/generate.py +268 -85
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +535 -163
- qtype/semantic/resolver.py +63 -19
- qtype/semantic/visualize.py +50 -35
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/METADATA +22 -5
- qtype-0.1.7.dist-info/RECORD +137 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -464
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -95
- qtype/interpreter/batch/step.py +0 -63
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -179
- qtype/interpreter/chat/chat_api.py +0 -237
- qtype/interpreter/chat/vercel.py +0 -314
- qtype/interpreter/exceptions.py +0 -10
- qtype/interpreter/step.py +0 -67
- qtype/interpreter/steps/__init__.py +0 -0
- qtype/interpreter/steps/agent.py +0 -114
- qtype/interpreter/steps/condition.py +0 -36
- qtype/interpreter/steps/decoder.py +0 -88
- qtype/interpreter/steps/llm_inference.py +0 -150
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -53
- qtype/interpreter/streaming_helpers.py +0 -123
- qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
- qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
- qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
- qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/loader.py +0 -389
- qtype-0.0.12.dist-info/RECORD +0 -105
- /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/WHEEL +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generic authorization context manager for QType interpreter.
|
|
3
3
|
|
|
4
|
-
This module provides a unified context manager that
|
|
5
|
-
type and
|
|
4
|
+
This module provides a unified `auth()` context manager that handles any
|
|
5
|
+
AuthorizationProvider type and returns the appropriate session or provider
|
|
6
|
+
instance with secrets resolved.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Automatic secret resolution for auth credentials using SecretManager
|
|
10
|
+
- Unified interface for all auth provider types (AWS, API Key, OAuth2)
|
|
11
|
+
- Returns authenticated sessions ready for use with external services
|
|
12
|
+
|
|
13
|
+
The context manager automatically:
|
|
14
|
+
1. Detects the auth provider type
|
|
15
|
+
2. Resolves any SecretReferences in credentials
|
|
16
|
+
3. Creates appropriate authentication sessions/objects
|
|
17
|
+
4. Handles cleanup when exiting the context
|
|
18
|
+
|
|
19
|
+
Supported Auth Types:
|
|
20
|
+
- AWSAuthProvider: Returns boto3.Session for AWS services
|
|
21
|
+
- APIKeyAuthProvider: Returns provider with resolved API key
|
|
22
|
+
- OAuth2AuthProvider: Returns provider with resolved client secret
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
```python
|
|
26
|
+
from qtype.semantic.model import APIKeyAuthProvider, SecretReference
|
|
27
|
+
from qtype.interpreter.auth.generic import auth
|
|
28
|
+
|
|
29
|
+
# Auth with secret reference
|
|
30
|
+
api_auth = APIKeyAuthProvider(
|
|
31
|
+
id="openai",
|
|
32
|
+
type="api_key",
|
|
33
|
+
api_key=SecretReference(secret_name="my-app/openai-key")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
with auth(api_auth, secret_manager) as provider:
|
|
37
|
+
# provider.api_key contains the resolved secret
|
|
38
|
+
headers = {"Authorization": f"Bearer {provider.api_key}"}
|
|
39
|
+
```
|
|
6
40
|
"""
|
|
7
41
|
|
|
8
42
|
from __future__ import annotations
|
|
@@ -13,6 +47,7 @@ from typing import Generator
|
|
|
13
47
|
import boto3 # type: ignore[import-untyped]
|
|
14
48
|
|
|
15
49
|
from qtype.interpreter.auth.aws import aws
|
|
50
|
+
from qtype.interpreter.base.secrets import SecretManagerBase
|
|
16
51
|
from qtype.semantic.model import (
|
|
17
52
|
APIKeyAuthProvider,
|
|
18
53
|
AuthorizationProvider,
|
|
@@ -27,10 +62,56 @@ class UnsupportedAuthProviderError(Exception):
|
|
|
27
62
|
pass
|
|
28
63
|
|
|
29
64
|
|
|
65
|
+
def resolve_provider_secrets(
|
|
66
|
+
provider: AuthorizationProvider,
|
|
67
|
+
secret_manager: SecretManagerBase,
|
|
68
|
+
) -> AuthorizationProvider:
|
|
69
|
+
"""
|
|
70
|
+
Resolve all SecretReference fields in an auth provider.
|
|
71
|
+
|
|
72
|
+
This helper automatically detects and resolves any fields that contain
|
|
73
|
+
SecretReference objects, returning a copy of the provider with resolved
|
|
74
|
+
secret values. This eliminates duplication when handling different auth
|
|
75
|
+
provider types.
|
|
76
|
+
|
|
77
|
+
Note: Always returns a copy to ensure consistency, even when there are
|
|
78
|
+
no secrets to resolve. This prevents issues with object identity checks
|
|
79
|
+
in tests and ensures a clean separation between DSL and runtime objects.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
provider: Auth provider instance with potential SecretReferences
|
|
83
|
+
secret_manager: Secret manager to use for resolution
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Copy of the provider with all SecretReferences resolved to strings
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> provider = APIKeyAuthProvider(
|
|
90
|
+
... id="my_auth",
|
|
91
|
+
... api_key=SecretReference(secret_name="my-key")
|
|
92
|
+
... )
|
|
93
|
+
>>> resolved = resolve_provider_secrets(provider, secret_manager)
|
|
94
|
+
>>> assert isinstance(resolved.api_key, str)
|
|
95
|
+
"""
|
|
96
|
+
context = f"auth provider '{provider.id}'"
|
|
97
|
+
updates = {}
|
|
98
|
+
|
|
99
|
+
# Iterate over all fields and resolve any SecretReferences
|
|
100
|
+
for field_name, field_info in provider.model_fields.items():
|
|
101
|
+
value = getattr(provider, field_name)
|
|
102
|
+
# Check if value is a SecretReference by looking for secret_name attr
|
|
103
|
+
if hasattr(value, "secret_name"):
|
|
104
|
+
updates[field_name] = secret_manager(value, context)
|
|
105
|
+
|
|
106
|
+
# Always create a copy to ensure clean separation between DSL and runtime
|
|
107
|
+
return provider.model_copy(update=updates)
|
|
108
|
+
|
|
109
|
+
|
|
30
110
|
@contextmanager
|
|
31
111
|
def auth(
|
|
32
112
|
auth_provider: AuthorizationProvider,
|
|
33
|
-
|
|
113
|
+
secret_manager: SecretManagerBase,
|
|
114
|
+
) -> Generator[boto3.Session | AuthorizationProvider, None, None]:
|
|
34
115
|
"""
|
|
35
116
|
Create an appropriate session or provider instance based on the auth provider type.
|
|
36
117
|
|
|
@@ -38,17 +119,16 @@ def auth(
|
|
|
38
119
|
on the type of AuthorizationProvider:
|
|
39
120
|
- AWSAuthProvider: Returns a configured boto3.Session
|
|
40
121
|
- APIKeyAuthProvider: Returns the provider instance (contains the API key)
|
|
41
|
-
- OAuth2AuthProvider: Raises NotImplementedError (not yet supported)
|
|
42
122
|
|
|
43
123
|
Args:
|
|
44
124
|
auth_provider: AuthorizationProvider instance of any supported type
|
|
125
|
+
secret_manager: Secret manager for resolving SecretReferences
|
|
45
126
|
|
|
46
127
|
Yields:
|
|
47
128
|
boto3.Session | APIKeyAuthProvider: The appropriate session or provider instance
|
|
48
129
|
|
|
49
130
|
Raises:
|
|
50
131
|
UnsupportedAuthProviderError: When an unsupported provider type is used
|
|
51
|
-
NotImplementedError: When OAuth2AuthProvider is used (not yet implemented)
|
|
52
132
|
|
|
53
133
|
Example:
|
|
54
134
|
```python
|
|
@@ -81,23 +161,20 @@ def auth(
|
|
|
81
161
|
"""
|
|
82
162
|
if isinstance(auth_provider, AWSAuthProvider):
|
|
83
163
|
# Use AWS-specific context manager
|
|
84
|
-
with aws(auth_provider) as session:
|
|
164
|
+
with aws(auth_provider, secret_manager) as session:
|
|
85
165
|
yield session
|
|
86
166
|
|
|
87
|
-
elif isinstance(auth_provider, APIKeyAuthProvider):
|
|
88
|
-
# For
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
elif isinstance(auth_provider, OAuth2AuthProvider):
|
|
93
|
-
# OAuth2 not yet implemented
|
|
94
|
-
raise NotImplementedError(
|
|
95
|
-
f"OAuth2 authentication is not yet implemented for provider '{auth_provider.id}'"
|
|
167
|
+
elif isinstance(auth_provider, (APIKeyAuthProvider, OAuth2AuthProvider)):
|
|
168
|
+
# For non-AWS providers, resolve secrets and yield modified copy
|
|
169
|
+
resolved_provider = resolve_provider_secrets(
|
|
170
|
+
auth_provider, secret_manager
|
|
96
171
|
)
|
|
172
|
+
yield resolved_provider
|
|
97
173
|
|
|
98
174
|
else:
|
|
99
175
|
# Unknown provider type
|
|
100
176
|
raise UnsupportedAuthProviderError(
|
|
101
|
-
f"Unsupported authorization provider type:
|
|
177
|
+
f"Unsupported authorization provider type: "
|
|
178
|
+
f"{type(auth_provider).__name__} "
|
|
102
179
|
f"for provider '{auth_provider.id}'"
|
|
103
180
|
)
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from aiostream import stream
|
|
8
|
+
from openinference.semconv.trace import (
|
|
9
|
+
OpenInferenceSpanKindValues,
|
|
10
|
+
SpanAttributes,
|
|
11
|
+
)
|
|
12
|
+
from opentelemetry import context, trace
|
|
13
|
+
from opentelemetry.trace import Status, StatusCode
|
|
14
|
+
|
|
15
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
16
|
+
from qtype.interpreter.base.progress_tracker import ProgressTracker
|
|
17
|
+
from qtype.interpreter.base.step_cache import (
|
|
18
|
+
cache_key,
|
|
19
|
+
create_cache,
|
|
20
|
+
from_cache_value,
|
|
21
|
+
to_cache_value,
|
|
22
|
+
)
|
|
23
|
+
from qtype.interpreter.base.stream_emitter import StreamEmitter
|
|
24
|
+
from qtype.interpreter.types import FlowMessage
|
|
25
|
+
from qtype.semantic.model import SecretReference, Step
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StepExecutor(ABC):
|
|
31
|
+
"""
|
|
32
|
+
Base class for step executors that process individual messages.
|
|
33
|
+
|
|
34
|
+
This executor processes messages one at a time, supporting both sequential
|
|
35
|
+
and concurrent execution based on the step's concurrency_config.
|
|
36
|
+
|
|
37
|
+
**Execution Flow:**
|
|
38
|
+
1. Failed messages are filtered out and collected for re-emission
|
|
39
|
+
2. Valid messages are processed individually via `process_message()`
|
|
40
|
+
3. Messages can be processed concurrently based on num_workers configuration
|
|
41
|
+
4. Results are streamed back with progress updates
|
|
42
|
+
5. Failed messages are emitted first (ordering not guaranteed with successes)
|
|
43
|
+
6. Optional finalization step runs after all processing completes
|
|
44
|
+
|
|
45
|
+
**Subclass Requirements:**
|
|
46
|
+
- Must implement `process_message()` to handle individual message processing
|
|
47
|
+
- Can optionally implement `finalize()` for cleanup/terminal operations
|
|
48
|
+
- Can optionally override `span_kind` to set appropriate OpenInference span type
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
step: The semantic step model defining behavior and configuration
|
|
52
|
+
context: ExecutorContext with cross-cutting concerns
|
|
53
|
+
**dependencies: Executor-specific dependencies (e.g., LLM clients,
|
|
54
|
+
database connections). These are injected by the executor factory
|
|
55
|
+
and stored for use during execution.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Subclasses can override this to set the appropriate span kind
|
|
59
|
+
span_kind: OpenInferenceSpanKindValues = (
|
|
60
|
+
OpenInferenceSpanKindValues.UNKNOWN
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
step: Step,
|
|
66
|
+
context: ExecutorContext,
|
|
67
|
+
**dependencies: Any,
|
|
68
|
+
):
|
|
69
|
+
self.step = step
|
|
70
|
+
self.context = context
|
|
71
|
+
self.dependencies = dependencies
|
|
72
|
+
self.progress = ProgressTracker(step.id)
|
|
73
|
+
self.stream_emitter = StreamEmitter(step, context.on_stream_event)
|
|
74
|
+
# Hook for subclasses to customize the processing function
|
|
75
|
+
# Base uses process_message with telemetry wrapping,
|
|
76
|
+
# BatchedStepExecutor uses process_batch
|
|
77
|
+
self._processor = self._process_message_with_cache
|
|
78
|
+
# Convenience properties from context
|
|
79
|
+
self._secret_manager = context.secret_manager
|
|
80
|
+
self._tracer = context.tracer or trace.get_tracer(__name__)
|
|
81
|
+
|
|
82
|
+
def _resolve_secret(self, value: str | SecretReference) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Resolve a value that may be a string or a SecretReference.
|
|
85
|
+
|
|
86
|
+
This is a convenience wrapper that adds step context to error messages.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
value: Either a plain string or a SecretReference
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The resolved string value
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
SecretResolutionError: If secret resolution fails
|
|
96
|
+
"""
|
|
97
|
+
context = f"step '{self.step.id}'"
|
|
98
|
+
return self._secret_manager(value, context)
|
|
99
|
+
|
|
100
|
+
async def _filter_and_collect_errors(
|
|
101
|
+
self,
|
|
102
|
+
message_stream: AsyncIterator[FlowMessage],
|
|
103
|
+
failed_messages: list[FlowMessage],
|
|
104
|
+
) -> AsyncIterator[FlowMessage]:
|
|
105
|
+
"""
|
|
106
|
+
Filter out failed messages from the stream and collect them separately.
|
|
107
|
+
|
|
108
|
+
This allows failed messages to be re-emitted at the end of processing
|
|
109
|
+
while valid messages proceed through the execution pipeline.
|
|
110
|
+
|
|
111
|
+
Note: Progress tracking for errors is NOT done here - it's handled
|
|
112
|
+
in the main execute() loop to consolidate all progress updates.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
message_stream: The input stream of messages
|
|
116
|
+
failed_messages: List to collect failed messages into
|
|
117
|
+
|
|
118
|
+
Yields:
|
|
119
|
+
Only messages that have not failed
|
|
120
|
+
"""
|
|
121
|
+
async for msg in message_stream:
|
|
122
|
+
if msg.is_failed():
|
|
123
|
+
logger.debug(
|
|
124
|
+
"Skipping failed message for step %s: %s",
|
|
125
|
+
self.step.id,
|
|
126
|
+
msg.error,
|
|
127
|
+
)
|
|
128
|
+
failed_messages.append(msg)
|
|
129
|
+
else:
|
|
130
|
+
yield msg
|
|
131
|
+
|
|
132
|
+
def _prepare_message_stream(
|
|
133
|
+
self, valid_messages: AsyncIterator[FlowMessage]
|
|
134
|
+
) -> AsyncIterator[Any]:
|
|
135
|
+
"""
|
|
136
|
+
Prepare the valid message stream for processing.
|
|
137
|
+
|
|
138
|
+
This is a hook for subclasses to transform the message stream before
|
|
139
|
+
processing. The base implementation returns messages unchanged.
|
|
140
|
+
BatchedStepExecutor overrides this to chunk messages into batches.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
valid_messages: Stream of valid (non-failed) messages
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Transformed stream ready for processing (same type for base,
|
|
147
|
+
AsyncIterator[list[FlowMessage]] for batched)
|
|
148
|
+
"""
|
|
149
|
+
return valid_messages
|
|
150
|
+
|
|
151
|
+
async def execute(
|
|
152
|
+
self,
|
|
153
|
+
message_stream: AsyncIterator[FlowMessage],
|
|
154
|
+
) -> AsyncIterator[FlowMessage]:
|
|
155
|
+
"""
|
|
156
|
+
Execute the step with the given message stream.
|
|
157
|
+
|
|
158
|
+
This is the main execution pipeline that orchestrates message processing.
|
|
159
|
+
The specific behavior (individual vs batched) is controlled by
|
|
160
|
+
_prepare_message_stream() and self._processor.
|
|
161
|
+
|
|
162
|
+
The execution flow:
|
|
163
|
+
1. Start step boundary for visual grouping
|
|
164
|
+
2. Filter out failed messages and collect them
|
|
165
|
+
3. Prepare valid messages for processing (hook for batching)
|
|
166
|
+
4. Process messages with optional concurrency
|
|
167
|
+
5. Emit failed messages first (no ordering guarantee)
|
|
168
|
+
6. Emit processed results
|
|
169
|
+
7. Run finalization hook
|
|
170
|
+
8. End step boundary
|
|
171
|
+
9. Track progress for all emitted messages
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message_stream: Input stream of messages to process
|
|
175
|
+
Yields:
|
|
176
|
+
Processed messages, with failed messages emitted first
|
|
177
|
+
"""
|
|
178
|
+
# Start a span for tracking
|
|
179
|
+
# Note: We manually manage the span lifecycle to allow yielding
|
|
180
|
+
span = self._tracer.start_span(
|
|
181
|
+
f"step.{self.step.id}",
|
|
182
|
+
attributes={
|
|
183
|
+
"step.id": self.step.id,
|
|
184
|
+
"step.type": self.step.__class__.__name__,
|
|
185
|
+
SpanAttributes.OPENINFERENCE_SPAN_KIND: self.span_kind.value,
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Make this span the active context so child spans will nest under it
|
|
190
|
+
# Only attach if span is recording (i.e., real tracer is configured)
|
|
191
|
+
ctx = trace.set_span_in_context(span)
|
|
192
|
+
token = context.attach(ctx) if span.is_recording() else None
|
|
193
|
+
|
|
194
|
+
# Initialize the cache
|
|
195
|
+
# this is done once per execution so re-runs are fast
|
|
196
|
+
self.cache = create_cache(self.step.cache_config, self.step.id)
|
|
197
|
+
|
|
198
|
+
# Start step boundary for visual grouping in UI
|
|
199
|
+
async with self.stream_emitter.step_boundary():
|
|
200
|
+
try:
|
|
201
|
+
# Collect failed messages to re-emit at the end
|
|
202
|
+
failed_messages: list[FlowMessage] = []
|
|
203
|
+
valid_messages = self._filter_and_collect_errors(
|
|
204
|
+
message_stream, failed_messages
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Determine concurrency from step configuration
|
|
208
|
+
num_workers = 1
|
|
209
|
+
if hasattr(self.step, "concurrency_config") and (
|
|
210
|
+
self.step.concurrency_config is not None # type: ignore[attr-defined]
|
|
211
|
+
):
|
|
212
|
+
num_workers = (
|
|
213
|
+
self.step.concurrency_config.num_workers # type: ignore[attr-defined]
|
|
214
|
+
)
|
|
215
|
+
span.set_attribute("step.concurrency", num_workers)
|
|
216
|
+
|
|
217
|
+
# Prepare messages for processing (batching hook)
|
|
218
|
+
prepared_messages = self._prepare_message_stream(
|
|
219
|
+
valid_messages
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Apply processor with concurrency control
|
|
223
|
+
async def process_item(
|
|
224
|
+
item: Any, *args: Any
|
|
225
|
+
) -> AsyncIterator[FlowMessage]:
|
|
226
|
+
async for msg in self._processor(item):
|
|
227
|
+
yield msg
|
|
228
|
+
|
|
229
|
+
result_stream = stream.flatmap(
|
|
230
|
+
prepared_messages, process_item, task_limit=num_workers
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Combine all streams
|
|
234
|
+
async def emit_failed_messages() -> AsyncIterator[FlowMessage]:
|
|
235
|
+
for msg in failed_messages:
|
|
236
|
+
yield msg
|
|
237
|
+
|
|
238
|
+
all_results = stream.concat(
|
|
239
|
+
stream.iterate([result_stream, emit_failed_messages()])
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Track message counts for telemetry
|
|
243
|
+
message_count = 0
|
|
244
|
+
error_count = 0
|
|
245
|
+
|
|
246
|
+
# Stream results and track progress
|
|
247
|
+
async with all_results.stream() as streamer:
|
|
248
|
+
result: FlowMessage
|
|
249
|
+
async for result in streamer:
|
|
250
|
+
message_count += 1
|
|
251
|
+
if result.is_failed():
|
|
252
|
+
error_count += 1
|
|
253
|
+
self.progress.update_for_message(
|
|
254
|
+
result, self.context.on_progress
|
|
255
|
+
)
|
|
256
|
+
yield result
|
|
257
|
+
|
|
258
|
+
# Finalize and track those messages too
|
|
259
|
+
async for msg in self.finalize():
|
|
260
|
+
message_count += 1
|
|
261
|
+
if msg.is_failed():
|
|
262
|
+
error_count += 1
|
|
263
|
+
yield msg
|
|
264
|
+
|
|
265
|
+
# Close the cache
|
|
266
|
+
if self.cache:
|
|
267
|
+
self.cache.close()
|
|
268
|
+
|
|
269
|
+
# Record metrics in span
|
|
270
|
+
span.set_attribute("step.messages.total", message_count)
|
|
271
|
+
span.set_attribute("step.messages.errors", error_count)
|
|
272
|
+
|
|
273
|
+
# Set span status based on errors
|
|
274
|
+
if error_count > 0:
|
|
275
|
+
span.set_status(
|
|
276
|
+
Status(
|
|
277
|
+
StatusCode.ERROR,
|
|
278
|
+
f"{error_count} of {message_count} messages failed",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
span.set_status(Status(StatusCode.OK))
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# Record the exception and set error status
|
|
286
|
+
span.record_exception(e)
|
|
287
|
+
span.set_status(Status(StatusCode.ERROR, f"Step failed: {e}"))
|
|
288
|
+
raise
|
|
289
|
+
finally:
|
|
290
|
+
# Detach the context and end the span
|
|
291
|
+
# Only detach if we successfully attached (span was recording)
|
|
292
|
+
if token is not None:
|
|
293
|
+
context.detach(token)
|
|
294
|
+
span.end()
|
|
295
|
+
|
|
296
|
+
@abstractmethod
|
|
297
|
+
async def process_message(
|
|
298
|
+
self, message: FlowMessage
|
|
299
|
+
) -> AsyncIterator[FlowMessage]:
|
|
300
|
+
"""
|
|
301
|
+
Process a single message.
|
|
302
|
+
|
|
303
|
+
Subclasses MUST implement this method to define how individual
|
|
304
|
+
messages are processed.
|
|
305
|
+
|
|
306
|
+
This is a one-to-many operation: a single input message may yield
|
|
307
|
+
zero, one, or multiple output messages. For example, a document
|
|
308
|
+
splitter might yield multiple chunks from one document.
|
|
309
|
+
|
|
310
|
+
This method is automatically wrapped with telemetry tracing when
|
|
311
|
+
called through the executor's execution pipeline.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
message: The input message to process
|
|
315
|
+
|
|
316
|
+
Yields:
|
|
317
|
+
Zero or more processed messages
|
|
318
|
+
"""
|
|
319
|
+
yield # type: ignore[misc]
|
|
320
|
+
|
|
321
|
+
async def _process_message_with_cache(
|
|
322
|
+
self, message: FlowMessage
|
|
323
|
+
) -> AsyncIterator[FlowMessage]:
|
|
324
|
+
downstream = self._process_message_with_telemetry
|
|
325
|
+
if self.cache is None:
|
|
326
|
+
async for output_msg in downstream(message):
|
|
327
|
+
yield output_msg
|
|
328
|
+
else:
|
|
329
|
+
key = cache_key(message, self.step)
|
|
330
|
+
cached_result = self.cache.get(key)
|
|
331
|
+
if cached_result is not None:
|
|
332
|
+
result = [from_cache_value(d, message) for d in cached_result] # type: ignore
|
|
333
|
+
self.progress.increment_cache(
|
|
334
|
+
self.context.on_progress,
|
|
335
|
+
hit_delta=len(result),
|
|
336
|
+
miss_delta=0,
|
|
337
|
+
)
|
|
338
|
+
# cache hit
|
|
339
|
+
for msg in result:
|
|
340
|
+
yield msg
|
|
341
|
+
else:
|
|
342
|
+
# cache miss -- process downstream and store result
|
|
343
|
+
buf = []
|
|
344
|
+
async for output_msg in downstream(message):
|
|
345
|
+
buf.append(output_msg)
|
|
346
|
+
yield output_msg
|
|
347
|
+
|
|
348
|
+
self.progress.increment_cache(
|
|
349
|
+
self.context.on_progress, hit_delta=0, miss_delta=len(buf)
|
|
350
|
+
)
|
|
351
|
+
# store the results in the cache of there are no errors or if instructed to do so
|
|
352
|
+
if (
|
|
353
|
+
all(not msg.is_failed() for msg in buf)
|
|
354
|
+
or self.step.cache_config.on_error == "Cache" # type: ignore
|
|
355
|
+
):
|
|
356
|
+
serialized = [to_cache_value(m, self.step) for m in buf]
|
|
357
|
+
self.cache.set(
|
|
358
|
+
key, serialized, expire=self.step.cache_config.ttl
|
|
359
|
+
) # type: ignore
|
|
360
|
+
|
|
361
|
+
async def _process_message_with_telemetry(
|
|
362
|
+
self, message: FlowMessage
|
|
363
|
+
) -> AsyncIterator[FlowMessage]:
|
|
364
|
+
"""
|
|
365
|
+
Internal wrapper that adds telemetry tracing to process_message.
|
|
366
|
+
|
|
367
|
+
This method creates a child span for each message processing
|
|
368
|
+
operation, automatically recording errors and success metrics.
|
|
369
|
+
The child span will automatically be nested under the current
|
|
370
|
+
active span in the context.
|
|
371
|
+
"""
|
|
372
|
+
# Get current context and create child span within it
|
|
373
|
+
span = self._tracer.start_span(
|
|
374
|
+
f"step.{self.step.id}.process_message",
|
|
375
|
+
attributes={
|
|
376
|
+
"session.id": message.session.session_id,
|
|
377
|
+
},
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
output_count = 0
|
|
382
|
+
error_occurred = False
|
|
383
|
+
|
|
384
|
+
async for output_msg in self.process_message(message):
|
|
385
|
+
output_count += 1
|
|
386
|
+
if output_msg.is_failed():
|
|
387
|
+
error_occurred = True
|
|
388
|
+
span.add_event(
|
|
389
|
+
"message_failed",
|
|
390
|
+
{
|
|
391
|
+
"error": str(output_msg.error),
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
yield output_msg
|
|
395
|
+
|
|
396
|
+
# Record processing metrics
|
|
397
|
+
span.set_attribute("message.outputs", output_count)
|
|
398
|
+
|
|
399
|
+
if error_occurred:
|
|
400
|
+
span.set_status(
|
|
401
|
+
Status(StatusCode.ERROR, "Message processing had errors")
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
span.set_status(Status(StatusCode.OK))
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
span.record_exception(e)
|
|
408
|
+
span.set_status(
|
|
409
|
+
Status(StatusCode.ERROR, f"Processing failed: {e}")
|
|
410
|
+
)
|
|
411
|
+
raise
|
|
412
|
+
finally:
|
|
413
|
+
span.end()
|
|
414
|
+
|
|
415
|
+
async def finalize(self) -> AsyncIterator[FlowMessage]:
|
|
416
|
+
"""
|
|
417
|
+
Optional finalization hook called after all input processing completes.
|
|
418
|
+
|
|
419
|
+
This method is called once after the input stream is exhausted and all
|
|
420
|
+
messages have been processed. It can be used for:
|
|
421
|
+
- Cleanup operations
|
|
422
|
+
- Emitting summary/aggregate results
|
|
423
|
+
- Flushing buffers
|
|
424
|
+
- Terminal operations (e.g., writing final output)
|
|
425
|
+
|
|
426
|
+
The default implementation yields nothing. Subclasses can override
|
|
427
|
+
to provide custom finalization behavior.
|
|
428
|
+
|
|
429
|
+
Yields:
|
|
430
|
+
Zero or more final messages to emit
|
|
431
|
+
"""
|
|
432
|
+
# Make this an async generator for type checking
|
|
433
|
+
# The return here makes this unreachable, but we need the yield
|
|
434
|
+
# to make this function an async generator
|
|
435
|
+
return
|
|
436
|
+
yield # type: ignore[unreachable]
|