ai-pipeline-core 0.1.6__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.
- ai_pipeline_core/__init__.py +54 -13
- ai_pipeline_core/documents/__init__.py +3 -0
- ai_pipeline_core/flow/__init__.py +5 -1
- ai_pipeline_core/flow/options.py +26 -0
- ai_pipeline_core/llm/client.py +5 -3
- ai_pipeline_core/pipeline.py +418 -0
- ai_pipeline_core/prefect.py +7 -0
- ai_pipeline_core/simple_runner/__init__.py +19 -0
- ai_pipeline_core/simple_runner/cli.py +95 -0
- ai_pipeline_core/simple_runner/simple_runner.py +147 -0
- ai_pipeline_core/tracing.py +63 -20
- {ai_pipeline_core-0.1.6.dist-info → ai_pipeline_core-0.1.7.dist-info}/METADATA +92 -30
- {ai_pipeline_core-0.1.6.dist-info → ai_pipeline_core-0.1.7.dist-info}/RECORD +15 -9
- {ai_pipeline_core-0.1.6.dist-info → ai_pipeline_core-0.1.7.dist-info}/WHEEL +0 -0
- {ai_pipeline_core-0.1.6.dist-info → ai_pipeline_core-0.1.7.dist-info}/licenses/LICENSE +0 -0
ai_pipeline_core/__init__.py
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
"""Pipeline Core - Shared infrastructure for AI pipelines."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
from .
|
|
3
|
+
from . import llm
|
|
4
|
+
from .documents import (
|
|
5
|
+
Document,
|
|
6
|
+
DocumentList,
|
|
7
|
+
FlowDocument,
|
|
8
|
+
TaskDocument,
|
|
9
|
+
canonical_name_key,
|
|
10
|
+
sanitize_url,
|
|
11
|
+
)
|
|
12
|
+
from .flow import FlowConfig, FlowOptions
|
|
13
|
+
from .llm import (
|
|
14
|
+
AIMessages,
|
|
15
|
+
AIMessageType,
|
|
16
|
+
ModelName,
|
|
17
|
+
ModelOptions,
|
|
18
|
+
ModelResponse,
|
|
19
|
+
StructuredModelResponse,
|
|
20
|
+
)
|
|
5
21
|
from .logging import (
|
|
6
22
|
LoggerMixin,
|
|
7
23
|
LoggingConfig,
|
|
@@ -9,28 +25,53 @@ from .logging import (
|
|
|
9
25
|
get_pipeline_logger,
|
|
10
26
|
setup_logging,
|
|
11
27
|
)
|
|
12
|
-
from .logging import
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
from .logging import get_pipeline_logger as get_logger
|
|
29
|
+
from .pipeline import pipeline_flow, pipeline_task
|
|
30
|
+
from .prefect import flow, task
|
|
15
31
|
from .prompt_manager import PromptManager
|
|
16
32
|
from .settings import settings
|
|
17
|
-
from .tracing import trace
|
|
33
|
+
from .tracing import TraceInfo, TraceLevel, trace
|
|
18
34
|
|
|
19
|
-
__version__ = "0.1.
|
|
35
|
+
__version__ = "0.1.7"
|
|
20
36
|
|
|
21
37
|
__all__ = [
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"FlowDocument",
|
|
38
|
+
# Config/Settings
|
|
39
|
+
"settings",
|
|
40
|
+
# Logging
|
|
26
41
|
"get_logger",
|
|
27
42
|
"get_pipeline_logger",
|
|
28
43
|
"LoggerMixin",
|
|
29
44
|
"LoggingConfig",
|
|
30
|
-
"PromptManager",
|
|
31
|
-
"settings",
|
|
32
45
|
"setup_logging",
|
|
33
46
|
"StructuredLoggerMixin",
|
|
47
|
+
# Documents
|
|
48
|
+
"Document",
|
|
49
|
+
"DocumentList",
|
|
50
|
+
"FlowDocument",
|
|
34
51
|
"TaskDocument",
|
|
52
|
+
"canonical_name_key",
|
|
53
|
+
"sanitize_url",
|
|
54
|
+
# Flow/Task
|
|
55
|
+
"FlowConfig",
|
|
56
|
+
"FlowOptions",
|
|
57
|
+
# Prefect decorators (clean, no tracing)
|
|
58
|
+
"task",
|
|
59
|
+
"flow",
|
|
60
|
+
# Pipeline decorators (with tracing)
|
|
61
|
+
"pipeline_task",
|
|
62
|
+
"pipeline_flow",
|
|
63
|
+
# LLM
|
|
64
|
+
"llm",
|
|
65
|
+
"ModelName",
|
|
66
|
+
"ModelOptions",
|
|
67
|
+
"ModelResponse",
|
|
68
|
+
"StructuredModelResponse",
|
|
69
|
+
"AIMessages",
|
|
70
|
+
"AIMessageType",
|
|
71
|
+
# Tracing
|
|
35
72
|
"trace",
|
|
73
|
+
"TraceLevel",
|
|
74
|
+
"TraceInfo",
|
|
75
|
+
# Utils
|
|
76
|
+
"PromptManager",
|
|
36
77
|
]
|
|
@@ -2,10 +2,13 @@ from .document import Document
|
|
|
2
2
|
from .document_list import DocumentList
|
|
3
3
|
from .flow_document import FlowDocument
|
|
4
4
|
from .task_document import TaskDocument
|
|
5
|
+
from .utils import canonical_name_key, sanitize_url
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"Document",
|
|
8
9
|
"DocumentList",
|
|
9
10
|
"FlowDocument",
|
|
10
11
|
"TaskDocument",
|
|
12
|
+
"canonical_name_key",
|
|
13
|
+
"sanitize_url",
|
|
11
14
|
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
|
|
6
|
+
from ai_pipeline_core.llm import ModelName
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound="FlowOptions")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlowOptions(BaseSettings):
|
|
12
|
+
"""Base configuration for AI Pipeline flows."""
|
|
13
|
+
|
|
14
|
+
core_model: ModelName | str = Field(
|
|
15
|
+
default="gpt-5",
|
|
16
|
+
description="Primary model for complex analysis and generation tasks.",
|
|
17
|
+
)
|
|
18
|
+
small_model: ModelName | str = Field(
|
|
19
|
+
default="gpt-5-mini",
|
|
20
|
+
description="Fast, cost-effective model for simple tasks and orchestration.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
model_config = SettingsConfigDict(frozen=True, extra="ignore")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["FlowOptions"]
|
ai_pipeline_core/llm/client.py
CHANGED
|
@@ -118,11 +118,13 @@ async def _generate_with_retry(
|
|
|
118
118
|
span.set_attributes(response.get_laminar_metadata())
|
|
119
119
|
Laminar.set_span_output(response.content)
|
|
120
120
|
if not response.content:
|
|
121
|
-
# disable cache in case of empty response
|
|
122
|
-
completion_kwargs["extra_body"]["cache"] = {"no-cache": True}
|
|
123
121
|
raise ValueError(f"Model {model} returned an empty response.")
|
|
124
122
|
return response
|
|
125
123
|
except (asyncio.TimeoutError, ValueError, Exception) as e:
|
|
124
|
+
if not isinstance(e, asyncio.TimeoutError):
|
|
125
|
+
# disable cache if it's not a timeout because it may cause an error
|
|
126
|
+
completion_kwargs["extra_body"]["cache"] = {"no-cache": True}
|
|
127
|
+
|
|
126
128
|
logger.warning(
|
|
127
129
|
"LLM generation failed (attempt %d/%d): %s",
|
|
128
130
|
attempt + 1,
|
|
@@ -167,7 +169,7 @@ T = TypeVar("T", bound=BaseModel)
|
|
|
167
169
|
|
|
168
170
|
@trace(ignore_inputs=["context"])
|
|
169
171
|
async def generate_structured(
|
|
170
|
-
model: ModelName,
|
|
172
|
+
model: ModelName | str,
|
|
171
173
|
response_format: type[T],
|
|
172
174
|
*,
|
|
173
175
|
context: AIMessages = AIMessages(),
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Pipeline decorators that combine Prefect functionality with tracing support.
|
|
2
|
+
|
|
3
|
+
These decorators extend the base Prefect decorators with automatic tracing capabilities.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
Coroutine,
|
|
14
|
+
Dict,
|
|
15
|
+
Iterable,
|
|
16
|
+
Optional,
|
|
17
|
+
TypeVar,
|
|
18
|
+
Union,
|
|
19
|
+
cast,
|
|
20
|
+
overload,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from prefect.assets import Asset
|
|
24
|
+
from prefect.cache_policies import CachePolicy
|
|
25
|
+
from prefect.context import TaskRunContext
|
|
26
|
+
from prefect.flows import Flow, FlowStateHook
|
|
27
|
+
from prefect.futures import PrefectFuture
|
|
28
|
+
from prefect.results import ResultSerializer, ResultStorage
|
|
29
|
+
from prefect.task_runners import TaskRunner
|
|
30
|
+
from prefect.tasks import (
|
|
31
|
+
RetryConditionCallable,
|
|
32
|
+
StateHookCallable,
|
|
33
|
+
Task,
|
|
34
|
+
TaskRunNameValueOrCallable,
|
|
35
|
+
)
|
|
36
|
+
from prefect.utilities.annotations import NotSet
|
|
37
|
+
from typing_extensions import Concatenate, ParamSpec
|
|
38
|
+
|
|
39
|
+
from ai_pipeline_core.documents import DocumentList
|
|
40
|
+
from ai_pipeline_core.flow.options import FlowOptions
|
|
41
|
+
from ai_pipeline_core.prefect import flow, task
|
|
42
|
+
from ai_pipeline_core.tracing import TraceLevel, trace
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
P = ParamSpec("P")
|
|
48
|
+
R = TypeVar("R")
|
|
49
|
+
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# PIPELINE TASK DECORATOR
|
|
52
|
+
# ============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
def pipeline_task(__fn: Callable[P, R], /) -> Task[P, R]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def pipeline_task(
|
|
61
|
+
*,
|
|
62
|
+
# Tracing parameters
|
|
63
|
+
trace_level: TraceLevel = "always",
|
|
64
|
+
trace_ignore_input: bool = False,
|
|
65
|
+
trace_ignore_output: bool = False,
|
|
66
|
+
trace_ignore_inputs: list[str] | None = None,
|
|
67
|
+
trace_input_formatter: Optional[Callable[..., str]] = None,
|
|
68
|
+
trace_output_formatter: Optional[Callable[..., str]] = None,
|
|
69
|
+
# Prefect parameters
|
|
70
|
+
name: Optional[str] = None,
|
|
71
|
+
description: Optional[str] = None,
|
|
72
|
+
tags: Optional[Iterable[str]] = None,
|
|
73
|
+
version: Optional[str] = None,
|
|
74
|
+
cache_policy: Union[CachePolicy, type[NotSet]] = NotSet,
|
|
75
|
+
cache_key_fn: Optional[Callable[[TaskRunContext, Dict[str, Any]], Optional[str]]] = None,
|
|
76
|
+
cache_expiration: Optional[datetime.timedelta] = None,
|
|
77
|
+
task_run_name: Optional[TaskRunNameValueOrCallable] = None,
|
|
78
|
+
retries: Optional[int] = None,
|
|
79
|
+
retry_delay_seconds: Optional[
|
|
80
|
+
Union[float, int, list[float], Callable[[int], list[float]]]
|
|
81
|
+
] = None,
|
|
82
|
+
retry_jitter_factor: Optional[float] = None,
|
|
83
|
+
persist_result: Optional[bool] = None,
|
|
84
|
+
result_storage: Optional[Union[ResultStorage, str]] = None,
|
|
85
|
+
result_serializer: Optional[Union[ResultSerializer, str]] = None,
|
|
86
|
+
result_storage_key: Optional[str] = None,
|
|
87
|
+
cache_result_in_memory: bool = True,
|
|
88
|
+
timeout_seconds: Union[int, float, None] = None,
|
|
89
|
+
log_prints: Optional[bool] = False,
|
|
90
|
+
refresh_cache: Optional[bool] = None,
|
|
91
|
+
on_completion: Optional[list[StateHookCallable]] = None,
|
|
92
|
+
on_failure: Optional[list[StateHookCallable]] = None,
|
|
93
|
+
retry_condition_fn: Optional[RetryConditionCallable] = None,
|
|
94
|
+
viz_return_value: Optional[bool] = None,
|
|
95
|
+
asset_deps: Optional[list[Union[str, Asset]]] = None,
|
|
96
|
+
) -> Callable[[Callable[P, R]], Task[P, R]]: ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def pipeline_task(
|
|
100
|
+
__fn: Optional[Callable[P, R]] = None,
|
|
101
|
+
/,
|
|
102
|
+
*,
|
|
103
|
+
# Tracing parameters
|
|
104
|
+
trace_level: TraceLevel = "always",
|
|
105
|
+
trace_ignore_input: bool = False,
|
|
106
|
+
trace_ignore_output: bool = False,
|
|
107
|
+
trace_ignore_inputs: list[str] | None = None,
|
|
108
|
+
trace_input_formatter: Optional[Callable[..., str]] = None,
|
|
109
|
+
trace_output_formatter: Optional[Callable[..., str]] = None,
|
|
110
|
+
# Prefect parameters
|
|
111
|
+
name: Optional[str] = None,
|
|
112
|
+
description: Optional[str] = None,
|
|
113
|
+
tags: Optional[Iterable[str]] = None,
|
|
114
|
+
version: Optional[str] = None,
|
|
115
|
+
cache_policy: Union[CachePolicy, type[NotSet]] = NotSet,
|
|
116
|
+
cache_key_fn: Optional[Callable[[TaskRunContext, Dict[str, Any]], Optional[str]]] = None,
|
|
117
|
+
cache_expiration: Optional[datetime.timedelta] = None,
|
|
118
|
+
task_run_name: Optional[TaskRunNameValueOrCallable] = None,
|
|
119
|
+
retries: Optional[int] = None,
|
|
120
|
+
retry_delay_seconds: Optional[
|
|
121
|
+
Union[float, int, list[float], Callable[[int], list[float]]]
|
|
122
|
+
] = None,
|
|
123
|
+
retry_jitter_factor: Optional[float] = None,
|
|
124
|
+
persist_result: Optional[bool] = None,
|
|
125
|
+
result_storage: Optional[Union[ResultStorage, str]] = None,
|
|
126
|
+
result_serializer: Optional[Union[ResultSerializer, str]] = None,
|
|
127
|
+
result_storage_key: Optional[str] = None,
|
|
128
|
+
cache_result_in_memory: bool = True,
|
|
129
|
+
timeout_seconds: Union[int, float, None] = None,
|
|
130
|
+
log_prints: Optional[bool] = False,
|
|
131
|
+
refresh_cache: Optional[bool] = None,
|
|
132
|
+
on_completion: Optional[list[StateHookCallable]] = None,
|
|
133
|
+
on_failure: Optional[list[StateHookCallable]] = None,
|
|
134
|
+
retry_condition_fn: Optional[RetryConditionCallable] = None,
|
|
135
|
+
viz_return_value: Optional[bool] = None,
|
|
136
|
+
asset_deps: Optional[list[Union[str, Asset]]] = None,
|
|
137
|
+
) -> Union[Task[P, R], Callable[[Callable[P, R]], Task[P, R]]]:
|
|
138
|
+
"""
|
|
139
|
+
Pipeline task decorator that combines Prefect task functionality with automatic tracing.
|
|
140
|
+
|
|
141
|
+
This decorator applies tracing before the Prefect task decorator, allowing you to
|
|
142
|
+
monitor task execution with LMNR while maintaining all Prefect functionality.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
trace_level: Control tracing ("always", "debug", "off")
|
|
146
|
+
trace_ignore_input: Whether to ignore input in traces
|
|
147
|
+
trace_ignore_output: Whether to ignore output in traces
|
|
148
|
+
trace_ignore_inputs: List of input parameter names to ignore
|
|
149
|
+
trace_input_formatter: Custom formatter for inputs
|
|
150
|
+
trace_output_formatter: Custom formatter for outputs
|
|
151
|
+
|
|
152
|
+
Plus all standard Prefect task parameters...
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def decorator(fn: Callable[P, R]) -> Task[P, R]:
|
|
156
|
+
# Apply tracing first if enabled
|
|
157
|
+
if trace_level != "off":
|
|
158
|
+
traced_fn = trace(
|
|
159
|
+
level=trace_level,
|
|
160
|
+
name=name or fn.__name__,
|
|
161
|
+
ignore_input=trace_ignore_input,
|
|
162
|
+
ignore_output=trace_ignore_output,
|
|
163
|
+
ignore_inputs=trace_ignore_inputs,
|
|
164
|
+
input_formatter=trace_input_formatter,
|
|
165
|
+
output_formatter=trace_output_formatter,
|
|
166
|
+
)(fn)
|
|
167
|
+
else:
|
|
168
|
+
traced_fn = fn
|
|
169
|
+
|
|
170
|
+
# Then apply Prefect task decorator
|
|
171
|
+
return task( # pyright: ignore[reportCallIssue,reportUnknownVariableType]
|
|
172
|
+
traced_fn, # pyright: ignore[reportArgumentType]
|
|
173
|
+
name=name,
|
|
174
|
+
description=description,
|
|
175
|
+
tags=tags,
|
|
176
|
+
version=version,
|
|
177
|
+
cache_policy=cache_policy,
|
|
178
|
+
cache_key_fn=cache_key_fn,
|
|
179
|
+
cache_expiration=cache_expiration,
|
|
180
|
+
task_run_name=task_run_name,
|
|
181
|
+
retries=retries or 0,
|
|
182
|
+
retry_delay_seconds=retry_delay_seconds,
|
|
183
|
+
retry_jitter_factor=retry_jitter_factor,
|
|
184
|
+
persist_result=persist_result,
|
|
185
|
+
result_storage=result_storage,
|
|
186
|
+
result_serializer=result_serializer,
|
|
187
|
+
result_storage_key=result_storage_key,
|
|
188
|
+
cache_result_in_memory=cache_result_in_memory,
|
|
189
|
+
timeout_seconds=timeout_seconds,
|
|
190
|
+
log_prints=log_prints,
|
|
191
|
+
refresh_cache=refresh_cache,
|
|
192
|
+
on_completion=on_completion,
|
|
193
|
+
on_failure=on_failure,
|
|
194
|
+
retry_condition_fn=retry_condition_fn,
|
|
195
|
+
viz_return_value=viz_return_value,
|
|
196
|
+
asset_deps=asset_deps,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if __fn:
|
|
200
|
+
return decorator(__fn)
|
|
201
|
+
return decorator
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ============================================================================
|
|
205
|
+
# PIPELINE FLOW DECORATOR WITH DOCUMENT PROCESSING
|
|
206
|
+
# ============================================================================
|
|
207
|
+
|
|
208
|
+
# Type aliases for document flow signatures
|
|
209
|
+
DocumentsFlowSig = Callable[
|
|
210
|
+
Concatenate[str, DocumentList, FlowOptions, P],
|
|
211
|
+
Union[DocumentList, Coroutine[Any, Any, DocumentList]],
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
DocumentsFlowResult = Flow[Concatenate[str, DocumentList, FlowOptions, P], DocumentList]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@overload
|
|
218
|
+
def pipeline_flow(
|
|
219
|
+
__fn: DocumentsFlowSig[P],
|
|
220
|
+
/,
|
|
221
|
+
) -> DocumentsFlowResult[P]: ...
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@overload
|
|
225
|
+
def pipeline_flow(
|
|
226
|
+
*,
|
|
227
|
+
# Tracing parameters
|
|
228
|
+
trace_level: TraceLevel = "always",
|
|
229
|
+
trace_ignore_input: bool = False,
|
|
230
|
+
trace_ignore_output: bool = False,
|
|
231
|
+
trace_ignore_inputs: list[str] | None = None,
|
|
232
|
+
trace_input_formatter: Optional[Callable[..., str]] = None,
|
|
233
|
+
trace_output_formatter: Optional[Callable[..., str]] = None,
|
|
234
|
+
# Prefect parameters
|
|
235
|
+
name: Optional[str] = None,
|
|
236
|
+
version: Optional[str] = None,
|
|
237
|
+
flow_run_name: Optional[Union[Callable[[], str], str]] = None,
|
|
238
|
+
retries: Optional[int] = None,
|
|
239
|
+
retry_delay_seconds: Optional[Union[int, float]] = None,
|
|
240
|
+
task_runner: Optional[TaskRunner[PrefectFuture[Any]]] = None,
|
|
241
|
+
description: Optional[str] = None,
|
|
242
|
+
timeout_seconds: Union[int, float, None] = None,
|
|
243
|
+
validate_parameters: bool = True,
|
|
244
|
+
persist_result: Optional[bool] = None,
|
|
245
|
+
result_storage: Optional[Union[ResultStorage, str]] = None,
|
|
246
|
+
result_serializer: Optional[Union[ResultSerializer, str]] = None,
|
|
247
|
+
cache_result_in_memory: bool = True,
|
|
248
|
+
log_prints: Optional[bool] = None,
|
|
249
|
+
on_completion: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
250
|
+
on_failure: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
251
|
+
on_cancellation: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
252
|
+
on_crashed: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
253
|
+
on_running: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
254
|
+
) -> Callable[[DocumentsFlowSig[P]], DocumentsFlowResult[P]]: ...
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def pipeline_flow(
|
|
258
|
+
__fn: Optional[DocumentsFlowSig[P]] = None,
|
|
259
|
+
/,
|
|
260
|
+
*,
|
|
261
|
+
# Tracing parameters
|
|
262
|
+
trace_level: TraceLevel = "always",
|
|
263
|
+
trace_ignore_input: bool = False,
|
|
264
|
+
trace_ignore_output: bool = False,
|
|
265
|
+
trace_ignore_inputs: list[str] | None = None,
|
|
266
|
+
trace_input_formatter: Optional[Callable[..., str]] = None,
|
|
267
|
+
trace_output_formatter: Optional[Callable[..., str]] = None,
|
|
268
|
+
# Prefect parameters
|
|
269
|
+
name: Optional[str] = None,
|
|
270
|
+
version: Optional[str] = None,
|
|
271
|
+
flow_run_name: Optional[Union[Callable[[], str], str]] = None,
|
|
272
|
+
retries: Optional[int] = None,
|
|
273
|
+
retry_delay_seconds: Optional[Union[int, float]] = None,
|
|
274
|
+
task_runner: Optional[TaskRunner[PrefectFuture[Any]]] = None,
|
|
275
|
+
description: Optional[str] = None,
|
|
276
|
+
timeout_seconds: Union[int, float, None] = None,
|
|
277
|
+
validate_parameters: bool = True,
|
|
278
|
+
persist_result: Optional[bool] = None,
|
|
279
|
+
result_storage: Optional[Union[ResultStorage, str]] = None,
|
|
280
|
+
result_serializer: Optional[Union[ResultSerializer, str]] = None,
|
|
281
|
+
cache_result_in_memory: bool = True,
|
|
282
|
+
log_prints: Optional[bool] = None,
|
|
283
|
+
on_completion: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
284
|
+
on_failure: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
285
|
+
on_cancellation: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
286
|
+
on_crashed: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
287
|
+
on_running: Optional[list["FlowStateHook[..., Any]"]] = None,
|
|
288
|
+
) -> Union[DocumentsFlowResult[P], Callable[[DocumentsFlowSig[P]], DocumentsFlowResult[P]]]:
|
|
289
|
+
"""
|
|
290
|
+
Pipeline flow for document processing with standardized signature.
|
|
291
|
+
|
|
292
|
+
This decorator enforces a specific signature for document processing flows:
|
|
293
|
+
- First parameter: project_name (str)
|
|
294
|
+
- Second parameter: documents (DocumentList)
|
|
295
|
+
- Third parameter: flow_options (FlowOptions or subclass)
|
|
296
|
+
- Additional parameters allowed
|
|
297
|
+
- Must return DocumentList
|
|
298
|
+
|
|
299
|
+
It includes automatic tracing and all Prefect flow functionality.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
trace_level: Control tracing ("always", "debug", "off")
|
|
303
|
+
trace_ignore_input: Whether to ignore input in traces
|
|
304
|
+
trace_ignore_output: Whether to ignore output in traces
|
|
305
|
+
trace_ignore_inputs: List of input parameter names to ignore
|
|
306
|
+
trace_input_formatter: Custom formatter for inputs
|
|
307
|
+
trace_output_formatter: Custom formatter for outputs
|
|
308
|
+
|
|
309
|
+
Plus all standard Prefect flow parameters...
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def decorator(func: DocumentsFlowSig[P]) -> DocumentsFlowResult[P]:
|
|
313
|
+
sig = inspect.signature(func)
|
|
314
|
+
params = list(sig.parameters.values())
|
|
315
|
+
|
|
316
|
+
if len(params) < 3:
|
|
317
|
+
raise TypeError(
|
|
318
|
+
f"@pipeline_flow '{func.__name__}' must accept at least 3 arguments: "
|
|
319
|
+
"(project_name, documents, flow_options)"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Validate parameter types (optional but recommended)
|
|
323
|
+
# We check names as a convention, not strict type checking at decoration time
|
|
324
|
+
expected_names = ["project_name", "documents", "flow_options"]
|
|
325
|
+
for i, expected in enumerate(expected_names):
|
|
326
|
+
if i < len(params) and params[i].name != expected:
|
|
327
|
+
print(
|
|
328
|
+
f"Warning: Parameter {i + 1} of '{func.__name__}' is named '{params[i].name}' "
|
|
329
|
+
f"but convention suggests '{expected}'"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Create wrapper that ensures return type
|
|
333
|
+
if inspect.iscoroutinefunction(func):
|
|
334
|
+
|
|
335
|
+
@functools.wraps(func)
|
|
336
|
+
async def wrapper( # pyright: ignore[reportRedeclaration]
|
|
337
|
+
project_name: str,
|
|
338
|
+
documents: DocumentList,
|
|
339
|
+
flow_options: FlowOptions,
|
|
340
|
+
*args, # pyright: ignore[reportMissingParameterType]
|
|
341
|
+
**kwargs, # pyright: ignore[reportMissingParameterType]
|
|
342
|
+
) -> DocumentList:
|
|
343
|
+
result = await func(project_name, documents, flow_options, *args, **kwargs)
|
|
344
|
+
# Runtime type checking
|
|
345
|
+
DL = DocumentList # Avoid recomputation
|
|
346
|
+
if not isinstance(result, DL):
|
|
347
|
+
raise TypeError(
|
|
348
|
+
f"Flow '{func.__name__}' must return a DocumentList, "
|
|
349
|
+
f"but returned {type(result).__name__}"
|
|
350
|
+
)
|
|
351
|
+
return result
|
|
352
|
+
else:
|
|
353
|
+
|
|
354
|
+
@functools.wraps(func)
|
|
355
|
+
def wrapper( # pyright: ignore[reportRedeclaration]
|
|
356
|
+
project_name: str,
|
|
357
|
+
documents: DocumentList,
|
|
358
|
+
flow_options: FlowOptions,
|
|
359
|
+
*args, # pyright: ignore[reportMissingParameterType]
|
|
360
|
+
**kwargs, # pyright: ignore[reportMissingParameterType]
|
|
361
|
+
) -> DocumentList:
|
|
362
|
+
result = func(project_name, documents, flow_options, *args, **kwargs)
|
|
363
|
+
# Runtime type checking
|
|
364
|
+
DL = DocumentList # Avoid recomputation
|
|
365
|
+
if not isinstance(result, DL):
|
|
366
|
+
raise TypeError(
|
|
367
|
+
f"Flow '{func.__name__}' must return a DocumentList, "
|
|
368
|
+
f"but returned {type(result).__name__}"
|
|
369
|
+
)
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
# Apply tracing first if enabled
|
|
373
|
+
if trace_level != "off":
|
|
374
|
+
traced_wrapper = trace(
|
|
375
|
+
level=trace_level,
|
|
376
|
+
name=name or func.__name__,
|
|
377
|
+
ignore_input=trace_ignore_input,
|
|
378
|
+
ignore_output=trace_ignore_output,
|
|
379
|
+
ignore_inputs=trace_ignore_inputs,
|
|
380
|
+
input_formatter=trace_input_formatter,
|
|
381
|
+
output_formatter=trace_output_formatter,
|
|
382
|
+
)(wrapper)
|
|
383
|
+
else:
|
|
384
|
+
traced_wrapper = wrapper
|
|
385
|
+
|
|
386
|
+
# Then apply Prefect flow decorator
|
|
387
|
+
return cast(
|
|
388
|
+
DocumentsFlowResult[P],
|
|
389
|
+
flow( # pyright: ignore[reportCallIssue,reportUnknownVariableType]
|
|
390
|
+
traced_wrapper, # pyright: ignore[reportArgumentType]
|
|
391
|
+
name=name,
|
|
392
|
+
version=version,
|
|
393
|
+
flow_run_name=flow_run_name,
|
|
394
|
+
retries=retries,
|
|
395
|
+
retry_delay_seconds=retry_delay_seconds,
|
|
396
|
+
task_runner=task_runner,
|
|
397
|
+
description=description,
|
|
398
|
+
timeout_seconds=timeout_seconds,
|
|
399
|
+
validate_parameters=validate_parameters,
|
|
400
|
+
persist_result=persist_result,
|
|
401
|
+
result_storage=result_storage,
|
|
402
|
+
result_serializer=result_serializer,
|
|
403
|
+
cache_result_in_memory=cache_result_in_memory,
|
|
404
|
+
log_prints=log_prints,
|
|
405
|
+
on_completion=on_completion,
|
|
406
|
+
on_failure=on_failure,
|
|
407
|
+
on_cancellation=on_cancellation,
|
|
408
|
+
on_crashed=on_crashed,
|
|
409
|
+
on_running=on_running,
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if __fn:
|
|
414
|
+
return decorator(__fn)
|
|
415
|
+
return decorator
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
__all__ = ["pipeline_task", "pipeline_flow"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .cli import run_cli
|
|
2
|
+
from .simple_runner import (
|
|
3
|
+
ConfigSequence,
|
|
4
|
+
FlowSequence,
|
|
5
|
+
load_documents_from_directory,
|
|
6
|
+
run_pipeline,
|
|
7
|
+
run_pipelines,
|
|
8
|
+
save_documents_to_directory,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"run_cli",
|
|
13
|
+
"run_pipeline",
|
|
14
|
+
"run_pipelines",
|
|
15
|
+
"load_documents_from_directory",
|
|
16
|
+
"save_documents_to_directory",
|
|
17
|
+
"FlowSequence",
|
|
18
|
+
"ConfigSequence",
|
|
19
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Type, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from lmnr import Laminar
|
|
8
|
+
from pydantic_settings import CliPositionalArg, SettingsConfigDict
|
|
9
|
+
|
|
10
|
+
from ai_pipeline_core.documents import DocumentList
|
|
11
|
+
from ai_pipeline_core.flow.options import FlowOptions
|
|
12
|
+
from ai_pipeline_core.logging import get_pipeline_logger, setup_logging
|
|
13
|
+
|
|
14
|
+
from .simple_runner import ConfigSequence, FlowSequence, run_pipelines, save_documents_to_directory
|
|
15
|
+
|
|
16
|
+
logger = get_pipeline_logger(__name__)
|
|
17
|
+
|
|
18
|
+
TOptions = TypeVar("TOptions", bound=FlowOptions)
|
|
19
|
+
InitializerFunc = Callable[[FlowOptions], tuple[str, DocumentList]] | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _initialize_environment() -> None:
|
|
23
|
+
setup_logging()
|
|
24
|
+
try:
|
|
25
|
+
Laminar.initialize()
|
|
26
|
+
logger.info("LMNR tracing initialized.")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.warning(f"Failed to initialize LMNR tracing: {e}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_cli(
|
|
32
|
+
*,
|
|
33
|
+
flows: FlowSequence,
|
|
34
|
+
flow_configs: ConfigSequence,
|
|
35
|
+
options_cls: Type[TOptions],
|
|
36
|
+
initializer: InitializerFunc = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Parse CLI+env into options, then run the pipeline.
|
|
40
|
+
|
|
41
|
+
- working_directory: required positional arg
|
|
42
|
+
- --project-name: optional, defaults to directory name
|
|
43
|
+
- --start/--end: optional, 1-based step bounds
|
|
44
|
+
- all other flags come from options_cls (fields & Field descriptions)
|
|
45
|
+
"""
|
|
46
|
+
_initialize_environment()
|
|
47
|
+
|
|
48
|
+
class _RunnerOptions( # type: ignore[reportRedeclaration]
|
|
49
|
+
options_cls,
|
|
50
|
+
cli_parse_args=True,
|
|
51
|
+
cli_kebab_case=True,
|
|
52
|
+
cli_exit_on_error=False,
|
|
53
|
+
):
|
|
54
|
+
working_directory: CliPositionalArg[Path]
|
|
55
|
+
project_name: str | None = None
|
|
56
|
+
start: int = 1
|
|
57
|
+
end: int | None = None
|
|
58
|
+
|
|
59
|
+
model_config = SettingsConfigDict(frozen=True, extra="ignore")
|
|
60
|
+
|
|
61
|
+
opts = cast(FlowOptions, _RunnerOptions()) # type: ignore[reportCallIssue]
|
|
62
|
+
|
|
63
|
+
wd: Path = cast(Path, getattr(opts, "working_directory"))
|
|
64
|
+
wd.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Get project name from options or use directory basename
|
|
67
|
+
project_name = getattr(opts, "project_name", None)
|
|
68
|
+
if not project_name: # None or empty string
|
|
69
|
+
project_name = wd.name
|
|
70
|
+
|
|
71
|
+
# Ensure project_name is not empty
|
|
72
|
+
if not project_name:
|
|
73
|
+
raise ValueError("Project name cannot be empty")
|
|
74
|
+
|
|
75
|
+
# Use initializer if provided, otherwise use defaults
|
|
76
|
+
initial_documents = DocumentList([])
|
|
77
|
+
if initializer:
|
|
78
|
+
init_result = initializer(opts)
|
|
79
|
+
# Always expect tuple format from initializer
|
|
80
|
+
_, initial_documents = init_result # Ignore project name from initializer
|
|
81
|
+
|
|
82
|
+
if getattr(opts, "start", 1) == 1 and initial_documents:
|
|
83
|
+
save_documents_to_directory(wd, initial_documents)
|
|
84
|
+
|
|
85
|
+
asyncio.run(
|
|
86
|
+
run_pipelines(
|
|
87
|
+
project_name=project_name,
|
|
88
|
+
output_dir=wd,
|
|
89
|
+
flows=flows,
|
|
90
|
+
flow_configs=flow_configs,
|
|
91
|
+
flow_options=opts,
|
|
92
|
+
start_step=getattr(opts, "start", 1),
|
|
93
|
+
end_step=getattr(opts, "end", None),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Callable, Sequence, Type
|
|
3
|
+
|
|
4
|
+
from ai_pipeline_core.documents import Document, DocumentList, FlowDocument
|
|
5
|
+
from ai_pipeline_core.flow.config import FlowConfig
|
|
6
|
+
from ai_pipeline_core.flow.options import FlowOptions
|
|
7
|
+
from ai_pipeline_core.logging import get_pipeline_logger
|
|
8
|
+
|
|
9
|
+
logger = get_pipeline_logger(__name__)
|
|
10
|
+
|
|
11
|
+
FlowSequence = Sequence[Callable[..., Any]]
|
|
12
|
+
ConfigSequence = Sequence[Type[FlowConfig]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_documents_from_directory(
|
|
16
|
+
base_dir: Path, document_types: Sequence[Type[FlowDocument]]
|
|
17
|
+
) -> DocumentList:
|
|
18
|
+
"""Loads documents using canonical_name."""
|
|
19
|
+
documents = DocumentList()
|
|
20
|
+
|
|
21
|
+
for doc_class in document_types:
|
|
22
|
+
dir_name = doc_class.canonical_name()
|
|
23
|
+
type_dir = base_dir / dir_name
|
|
24
|
+
|
|
25
|
+
if not type_dir.exists() or not type_dir.is_dir():
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
logger.info(f"Loading documents from {type_dir.relative_to(base_dir)}")
|
|
29
|
+
|
|
30
|
+
for file_path in type_dir.iterdir():
|
|
31
|
+
if not file_path.is_file() or file_path.name.endswith(Document.DESCRIPTION_EXTENSION):
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
content = file_path.read_bytes()
|
|
36
|
+
doc = doc_class(name=file_path.name, content=content)
|
|
37
|
+
|
|
38
|
+
desc_file = file_path.with_name(file_path.name + Document.DESCRIPTION_EXTENSION)
|
|
39
|
+
if desc_file.exists():
|
|
40
|
+
object.__setattr__(doc, "description", desc_file.read_text(encoding="utf-8"))
|
|
41
|
+
|
|
42
|
+
documents.append(doc)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(
|
|
45
|
+
f" Failed to load {file_path.name} as {doc_class.__name__}: {e}", exc_info=True
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return documents
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def save_documents_to_directory(base_dir: Path, documents: DocumentList) -> None:
|
|
52
|
+
"""Saves documents using canonical_name."""
|
|
53
|
+
for document in documents:
|
|
54
|
+
if not isinstance(document, FlowDocument):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
dir_name = document.canonical_name()
|
|
58
|
+
document_dir = base_dir / dir_name
|
|
59
|
+
document_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
file_path = document_dir / document.name
|
|
62
|
+
file_path.write_bytes(document.content)
|
|
63
|
+
logger.info(f"Saved: {dir_name}/{document.name}")
|
|
64
|
+
|
|
65
|
+
if document.description:
|
|
66
|
+
desc_file = file_path.with_name(file_path.name + Document.DESCRIPTION_EXTENSION)
|
|
67
|
+
desc_file.write_text(document.description, encoding="utf-8")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def run_pipeline(
|
|
71
|
+
flow_func: Callable[..., Any],
|
|
72
|
+
config: Type[FlowConfig],
|
|
73
|
+
project_name: str,
|
|
74
|
+
output_dir: Path,
|
|
75
|
+
flow_options: FlowOptions,
|
|
76
|
+
flow_name: str | None = None,
|
|
77
|
+
) -> DocumentList:
|
|
78
|
+
"""Execute a single pipeline flow."""
|
|
79
|
+
if flow_name is None:
|
|
80
|
+
flow_name = getattr(flow_func, "name", getattr(flow_func, "__name__", "flow"))
|
|
81
|
+
|
|
82
|
+
logger.info(f"Running Flow: {flow_name}")
|
|
83
|
+
|
|
84
|
+
input_documents = load_documents_from_directory(output_dir, config.INPUT_DOCUMENT_TYPES)
|
|
85
|
+
|
|
86
|
+
if not config.has_input_documents(input_documents):
|
|
87
|
+
raise RuntimeError(f"Missing input documents for flow {flow_name}")
|
|
88
|
+
|
|
89
|
+
result_documents = await flow_func(project_name, input_documents, flow_options)
|
|
90
|
+
|
|
91
|
+
config.validate_output_documents(result_documents)
|
|
92
|
+
|
|
93
|
+
save_documents_to_directory(output_dir, result_documents)
|
|
94
|
+
|
|
95
|
+
logger.info(f"Completed Flow: {flow_name}")
|
|
96
|
+
|
|
97
|
+
return result_documents
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def run_pipelines(
|
|
101
|
+
project_name: str,
|
|
102
|
+
output_dir: Path,
|
|
103
|
+
flows: FlowSequence,
|
|
104
|
+
flow_configs: ConfigSequence,
|
|
105
|
+
flow_options: FlowOptions,
|
|
106
|
+
start_step: int = 1,
|
|
107
|
+
end_step: int | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Executes multiple pipeline flows sequentially."""
|
|
110
|
+
if len(flows) != len(flow_configs):
|
|
111
|
+
raise ValueError("The number of flows and flow configs must match.")
|
|
112
|
+
|
|
113
|
+
num_steps = len(flows)
|
|
114
|
+
start_index = start_step - 1
|
|
115
|
+
end_index = (end_step if end_step is not None else num_steps) - 1
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
not (0 <= start_index < num_steps)
|
|
119
|
+
or not (0 <= end_index < num_steps)
|
|
120
|
+
or start_index > end_index
|
|
121
|
+
):
|
|
122
|
+
raise ValueError("Invalid start/end steps.")
|
|
123
|
+
|
|
124
|
+
logger.info(f"Starting pipeline '{project_name}' (Steps {start_step} to {end_index + 1})")
|
|
125
|
+
|
|
126
|
+
for i in range(start_index, end_index + 1):
|
|
127
|
+
flow_func = flows[i]
|
|
128
|
+
config = flow_configs[i]
|
|
129
|
+
flow_name = getattr(flow_func, "name", getattr(flow_func, "__name__", f"flow_{i + 1}"))
|
|
130
|
+
|
|
131
|
+
logger.info(f"--- [Step {i + 1}/{num_steps}] Running Flow: {flow_name} ---")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
await run_pipeline(
|
|
135
|
+
flow_func=flow_func,
|
|
136
|
+
config=config,
|
|
137
|
+
project_name=project_name,
|
|
138
|
+
output_dir=output_dir,
|
|
139
|
+
flow_options=flow_options,
|
|
140
|
+
flow_name=f"[Step {i + 1}/{num_steps}] {flow_name}",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(
|
|
145
|
+
f"--- [Step {i + 1}/{num_steps}] Flow {flow_name} Failed: {e} ---", exc_info=True
|
|
146
|
+
)
|
|
147
|
+
raise
|
ai_pipeline_core/tracing.py
CHANGED
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import inspect
|
|
12
12
|
import os
|
|
13
13
|
from functools import wraps
|
|
14
|
-
from typing import Any, Callable, ParamSpec, TypeVar, cast, overload
|
|
14
|
+
from typing import Any, Callable, Literal, ParamSpec, TypeVar, cast, overload
|
|
15
15
|
|
|
16
16
|
from lmnr import Instruments, Laminar, observe
|
|
17
17
|
from pydantic import BaseModel
|
|
@@ -24,6 +24,8 @@ from ai_pipeline_core.settings import settings
|
|
|
24
24
|
P = ParamSpec("P")
|
|
25
25
|
R = TypeVar("R")
|
|
26
26
|
|
|
27
|
+
TraceLevel = Literal["always", "debug", "off"]
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
# ---------------------------------------------------------------------------
|
|
29
31
|
# ``TraceInfo`` – metadata container
|
|
@@ -67,22 +69,28 @@ def _initialise_laminar() -> None:
|
|
|
67
69
|
if settings.lmnr_project_api_key:
|
|
68
70
|
Laminar.initialize(
|
|
69
71
|
project_api_key=settings.lmnr_project_api_key,
|
|
70
|
-
disabled_instruments=[Instruments.OPENAI],
|
|
72
|
+
disabled_instruments=[Instruments.OPENAI] if Instruments.OPENAI else [],
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
|
|
74
|
-
# Overload for calls like @trace(name="...",
|
|
76
|
+
# Overload for calls like @trace(name="...", level="debug")
|
|
75
77
|
@overload
|
|
76
78
|
def trace(
|
|
77
79
|
*,
|
|
80
|
+
level: TraceLevel = "always",
|
|
78
81
|
name: str | None = None,
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
session_id: str | None = None,
|
|
83
|
+
user_id: str | None = None,
|
|
84
|
+
metadata: dict[str, Any] | None = None,
|
|
85
|
+
tags: list[str] | None = None,
|
|
86
|
+
span_type: str | None = None,
|
|
81
87
|
ignore_input: bool = False,
|
|
82
88
|
ignore_output: bool = False,
|
|
83
89
|
ignore_inputs: list[str] | None = None,
|
|
84
90
|
input_formatter: Callable[..., str] | None = None,
|
|
85
91
|
output_formatter: Callable[..., str] | None = None,
|
|
92
|
+
ignore_exceptions: bool = False,
|
|
93
|
+
preserve_global_context: bool = True,
|
|
86
94
|
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
87
95
|
|
|
88
96
|
|
|
@@ -95,56 +103,76 @@ def trace(func: Callable[P, R]) -> Callable[P, R]: ...
|
|
|
95
103
|
def trace(
|
|
96
104
|
func: Callable[P, R] | None = None,
|
|
97
105
|
*,
|
|
106
|
+
level: TraceLevel = "always",
|
|
98
107
|
name: str | None = None,
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
session_id: str | None = None,
|
|
109
|
+
user_id: str | None = None,
|
|
110
|
+
metadata: dict[str, Any] | None = None,
|
|
111
|
+
tags: list[str] | None = None,
|
|
112
|
+
span_type: str | None = None,
|
|
101
113
|
ignore_input: bool = False,
|
|
102
114
|
ignore_output: bool = False,
|
|
103
115
|
ignore_inputs: list[str] | None = None,
|
|
104
116
|
input_formatter: Callable[..., str] | None = None,
|
|
105
117
|
output_formatter: Callable[..., str] | None = None,
|
|
118
|
+
ignore_exceptions: bool = False,
|
|
106
119
|
preserve_global_context: bool = True,
|
|
107
120
|
) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
|
|
108
121
|
"""Decorator that wires Laminar tracing and observation into a function.
|
|
109
122
|
|
|
110
123
|
Args:
|
|
111
124
|
func: The function to be traced (when used as @trace)
|
|
125
|
+
level: Trace level control:
|
|
126
|
+
- "always": Always trace (default)
|
|
127
|
+
- "debug": Only trace when LMNR_DEBUG environment variable is NOT set to "true"
|
|
128
|
+
- "off": Never trace
|
|
112
129
|
name: Custom name for the observation (defaults to function name)
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
metadata: Additional metadata for the trace
|
|
131
|
+
tags: Additional tags for the trace
|
|
132
|
+
span_type: Type of span for the trace
|
|
115
133
|
ignore_input: Ignore all inputs in the trace
|
|
116
134
|
ignore_output: Ignore the output in the trace
|
|
117
135
|
ignore_inputs: List of specific input parameter names to ignore
|
|
118
136
|
input_formatter: Custom formatter for inputs (takes any arguments, returns string)
|
|
119
137
|
output_formatter: Custom formatter for outputs (takes any arguments, returns string)
|
|
138
|
+
ignore_exceptions: Whether to ignore exceptions in tracing
|
|
139
|
+
preserve_global_context: Whether to preserve global context
|
|
120
140
|
|
|
121
141
|
Returns:
|
|
122
142
|
The decorated function with Laminar tracing enabled
|
|
123
143
|
"""
|
|
124
144
|
|
|
145
|
+
if level == "off":
|
|
146
|
+
if func:
|
|
147
|
+
return func
|
|
148
|
+
return lambda f: f
|
|
149
|
+
|
|
125
150
|
def decorator(f: Callable[P, R]) -> Callable[P, R]:
|
|
151
|
+
# Handle 'debug' level logic - only trace when LMNR_DEBUG is NOT "true"
|
|
152
|
+
if level == "debug" and os.getenv("LMNR_DEBUG", "").lower() == "true":
|
|
153
|
+
return f
|
|
154
|
+
|
|
126
155
|
# --- Pre-computation (done once when the function is decorated) ---
|
|
127
156
|
_initialise_laminar()
|
|
128
157
|
sig = inspect.signature(f)
|
|
129
158
|
is_coroutine = inspect.iscoroutinefunction(f)
|
|
130
|
-
decorator_test_flag = test
|
|
131
159
|
observe_name = name or f.__name__
|
|
132
160
|
_observe = observe
|
|
133
161
|
|
|
134
162
|
# Store the new parameters
|
|
163
|
+
_session_id = session_id
|
|
164
|
+
_user_id = user_id
|
|
165
|
+
_metadata = metadata
|
|
166
|
+
_tags = tags or []
|
|
167
|
+
_span_type = span_type
|
|
135
168
|
_ignore_input = ignore_input
|
|
136
169
|
_ignore_output = ignore_output
|
|
137
170
|
_ignore_inputs = ignore_inputs
|
|
138
171
|
_input_formatter = input_formatter
|
|
139
172
|
_output_formatter = output_formatter
|
|
173
|
+
_ignore_exceptions = ignore_exceptions
|
|
140
174
|
_preserve_global_context = preserve_global_context
|
|
141
175
|
|
|
142
|
-
# --- Check debug_only flag and environment variable ---
|
|
143
|
-
if debug_only and os.getenv("LMNR_DEBUG", "").lower() != "true":
|
|
144
|
-
# If debug_only is True but LMNR_DEBUG is not set to "true",
|
|
145
|
-
# return the original function without tracing
|
|
146
|
-
return f
|
|
147
|
-
|
|
148
176
|
# --- Helper function for runtime logic ---
|
|
149
177
|
def _prepare_and_get_observe_params(runtime_kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
150
178
|
"""
|
|
@@ -157,13 +185,23 @@ def trace(
|
|
|
157
185
|
if "trace_info" in sig.parameters:
|
|
158
186
|
runtime_kwargs["trace_info"] = trace_info
|
|
159
187
|
|
|
160
|
-
runtime_test_flag = bool(runtime_kwargs.get("test", False))
|
|
161
|
-
if (decorator_test_flag or runtime_test_flag) and "test" not in trace_info.tags:
|
|
162
|
-
trace_info.tags.append("test")
|
|
163
|
-
|
|
164
188
|
observe_params = trace_info.get_observe_kwargs()
|
|
165
189
|
observe_params["name"] = observe_name
|
|
166
190
|
|
|
191
|
+
# Override with decorator-level session_id and user_id if provided
|
|
192
|
+
if _session_id:
|
|
193
|
+
observe_params["session_id"] = _session_id
|
|
194
|
+
if _user_id:
|
|
195
|
+
observe_params["user_id"] = _user_id
|
|
196
|
+
|
|
197
|
+
# Merge decorator-level metadata and tags
|
|
198
|
+
if _metadata:
|
|
199
|
+
observe_params["metadata"] = {**observe_params.get("metadata", {}), **_metadata}
|
|
200
|
+
if _tags:
|
|
201
|
+
observe_params["tags"] = observe_params.get("tags", []) + _tags
|
|
202
|
+
if _span_type:
|
|
203
|
+
observe_params["span_type"] = _span_type
|
|
204
|
+
|
|
167
205
|
# Add the new Laminar parameters
|
|
168
206
|
if _ignore_input:
|
|
169
207
|
observe_params["ignore_input"] = _ignore_input
|
|
@@ -175,6 +213,8 @@ def trace(
|
|
|
175
213
|
observe_params["input_formatter"] = _input_formatter
|
|
176
214
|
if _output_formatter is not None:
|
|
177
215
|
observe_params["output_formatter"] = _output_formatter
|
|
216
|
+
if _ignore_exceptions:
|
|
217
|
+
observe_params["ignore_exceptions"] = _ignore_exceptions
|
|
178
218
|
if _preserve_global_context:
|
|
179
219
|
observe_params["preserve_global_context"] = _preserve_global_context
|
|
180
220
|
|
|
@@ -207,3 +247,6 @@ def trace(
|
|
|
207
247
|
return decorator(func) # Called as @trace
|
|
208
248
|
else:
|
|
209
249
|
return decorator # Called as @trace(...)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
__all__ = ["trace", "TraceLevel", "TraceInfo"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-pipeline-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Core utilities for AI-powered processing pipelines using prefect
|
|
5
5
|
Project-URL: Homepage, https://github.com/bbarwik/ai-pipeline-core
|
|
6
6
|
Project-URL: Repository, https://github.com/bbarwik/ai-pipeline-core
|
|
@@ -20,7 +20,7 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Requires-Dist: httpx>=0.28.1
|
|
22
22
|
Requires-Dist: jinja2>=3.1.6
|
|
23
|
-
Requires-Dist: lmnr>=0.7.
|
|
23
|
+
Requires-Dist: lmnr>=0.7.6
|
|
24
24
|
Requires-Dist: openai>=1.99.9
|
|
25
25
|
Requires-Dist: prefect>=3.4.13
|
|
26
26
|
Requires-Dist: pydantic-settings>=2.10.1
|
|
@@ -151,40 +151,76 @@ async def process_document(doc: Document):
|
|
|
151
151
|
return response.parsed
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
###
|
|
154
|
+
### Enhanced Pipeline Decorators (New in v0.1.7)
|
|
155
155
|
```python
|
|
156
|
-
from
|
|
157
|
-
from ai_pipeline_core.
|
|
158
|
-
from ai_pipeline_core.
|
|
159
|
-
from ai_pipeline_core.tracing import trace
|
|
156
|
+
from ai_pipeline_core import pipeline_flow, pipeline_task
|
|
157
|
+
from ai_pipeline_core.flow import FlowOptions
|
|
158
|
+
from ai_pipeline_core.documents import DocumentList, FlowDocument
|
|
160
159
|
|
|
161
|
-
class
|
|
162
|
-
"""
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
class CustomFlowOptions(FlowOptions):
|
|
161
|
+
"""Extend base options with your custom fields"""
|
|
162
|
+
batch_size: int = 100
|
|
163
|
+
temperature: float = 0.7
|
|
165
164
|
|
|
166
|
-
|
|
167
|
-
INPUT_DOCUMENT_TYPES = [InputDocument]
|
|
168
|
-
OUTPUT_DOCUMENT_TYPE = OutputDocument
|
|
169
|
-
|
|
170
|
-
@task
|
|
171
|
-
@trace
|
|
165
|
+
@pipeline_task(trace_level="always", retries=3)
|
|
172
166
|
async def process_task(doc: Document) -> Document:
|
|
173
|
-
# Task
|
|
167
|
+
# Task with automatic tracing and retries
|
|
174
168
|
result = await process_document(doc)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
return OutputDocument(name="result", content=result.encode())
|
|
170
|
+
|
|
171
|
+
@pipeline_flow(trace_level="always")
|
|
172
|
+
async def my_pipeline(
|
|
173
|
+
project_name: str,
|
|
174
|
+
documents: DocumentList,
|
|
175
|
+
flow_options: CustomFlowOptions # Type-safe custom options
|
|
176
|
+
) -> DocumentList:
|
|
177
|
+
# Pipeline flow with enforced signature and tracing
|
|
178
|
+
results = []
|
|
179
|
+
for doc in documents:
|
|
180
|
+
result = await process_task(doc)
|
|
181
|
+
results.append(result)
|
|
182
|
+
return DocumentList(results)
|
|
183
|
+
```
|
|
178
184
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
### Simple Runner Utility (New in v0.1.7)
|
|
186
|
+
```python
|
|
187
|
+
from ai_pipeline_core.simple_runner import run_cli, run_pipeline
|
|
188
|
+
from ai_pipeline_core.flow import FlowOptions
|
|
189
|
+
|
|
190
|
+
# CLI-based pipeline execution
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
run_cli(
|
|
193
|
+
flows=[my_pipeline],
|
|
194
|
+
flow_configs=[MyFlowConfig],
|
|
195
|
+
options_cls=CustomFlowOptions
|
|
196
|
+
)
|
|
183
197
|
|
|
184
|
-
|
|
198
|
+
# Or programmatic execution
|
|
199
|
+
async def main():
|
|
200
|
+
result = await run_pipeline(
|
|
201
|
+
project_name="my-project",
|
|
202
|
+
output_dir=Path("./output"),
|
|
203
|
+
flow=my_pipeline,
|
|
204
|
+
flow_config=MyFlowConfig,
|
|
205
|
+
flow_options=CustomFlowOptions(batch_size=50)
|
|
206
|
+
)
|
|
207
|
+
```
|
|
185
208
|
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
### Clean Prefect Decorators (New in v0.1.7)
|
|
210
|
+
```python
|
|
211
|
+
# Import clean Prefect decorators without tracing
|
|
212
|
+
from ai_pipeline_core.prefect import flow, task
|
|
213
|
+
|
|
214
|
+
# Or use pipeline decorators with tracing
|
|
215
|
+
from ai_pipeline_core import pipeline_flow, pipeline_task
|
|
216
|
+
|
|
217
|
+
@task # Clean Prefect task
|
|
218
|
+
def compute(x: int) -> int:
|
|
219
|
+
return x * 2
|
|
220
|
+
|
|
221
|
+
@pipeline_task(trace_level="always") # With tracing
|
|
222
|
+
def compute_traced(x: int) -> int:
|
|
223
|
+
return x * 2
|
|
188
224
|
```
|
|
189
225
|
|
|
190
226
|
## Core Modules
|
|
@@ -291,8 +327,14 @@ ai_pipeline_core/
|
|
|
291
327
|
│ ├── client.py # Async client implementation
|
|
292
328
|
│ └── model_options.py # Configuration models
|
|
293
329
|
├── flow/ # Prefect flow utilities
|
|
294
|
-
│
|
|
330
|
+
│ ├── config.py # Type-safe flow configuration
|
|
331
|
+
│ └── options.py # FlowOptions base class (v0.1.7)
|
|
332
|
+
├── simple_runner/ # Pipeline execution utilities (v0.1.7)
|
|
333
|
+
│ ├── cli.py # CLI interface
|
|
334
|
+
│ └── simple_runner.py # Core runner logic
|
|
295
335
|
├── logging/ # Structured logging
|
|
336
|
+
├── pipeline.py # Enhanced decorators (v0.1.7)
|
|
337
|
+
├── prefect.py # Clean Prefect exports (v0.1.7)
|
|
296
338
|
├── tracing.py # Observability decorators
|
|
297
339
|
└── settings.py # Centralized configuration
|
|
298
340
|
```
|
|
@@ -469,9 +511,29 @@ Built with:
|
|
|
469
511
|
- [LiteLLM](https://litellm.ai/) - LLM proxy
|
|
470
512
|
- [Pydantic](https://pydantic-docs.helpmanual.io/) - Data validation
|
|
471
513
|
|
|
514
|
+
## What's New in v0.1.7
|
|
515
|
+
|
|
516
|
+
### Major Additions
|
|
517
|
+
- **Enhanced Pipeline Decorators**: New `pipeline_flow` and `pipeline_task` decorators combining Prefect functionality with automatic LMNR tracing
|
|
518
|
+
- **FlowOptions Base Class**: Extensible configuration system for flows with type-safe inheritance
|
|
519
|
+
- **Simple Runner Module**: CLI and programmatic utilities for easy pipeline execution
|
|
520
|
+
- **Clean Prefect Exports**: Separate imports for Prefect decorators with and without tracing
|
|
521
|
+
- **Expanded Exports**: All major components now accessible from top-level package import
|
|
522
|
+
|
|
523
|
+
### API Improvements
|
|
524
|
+
- Better type inference for document flows with custom options
|
|
525
|
+
- Support for custom FlowOptions inheritance in pipeline flows
|
|
526
|
+
- Improved error messages for invalid flow signatures
|
|
527
|
+
- Enhanced document utility functions (`canonical_name_key`, `sanitize_url`)
|
|
528
|
+
|
|
529
|
+
### Developer Experience
|
|
530
|
+
- Simplified imports - most components available from `ai_pipeline_core` directly
|
|
531
|
+
- Better separation of concerns between clean Prefect and traced pipeline decorators
|
|
532
|
+
- More intuitive flow configuration with `FlowOptions` inheritance
|
|
533
|
+
|
|
472
534
|
## Stability Notice
|
|
473
535
|
|
|
474
|
-
**Current Version**: 0.1.
|
|
536
|
+
**Current Version**: 0.1.7
|
|
475
537
|
**Status**: Internal Preview
|
|
476
538
|
**API Stability**: Unstable - Breaking changes expected
|
|
477
539
|
**Recommended Use**: Learning and reference only
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
ai_pipeline_core/__init__.py,sha256=
|
|
1
|
+
ai_pipeline_core/__init__.py,sha256=INcTtHr2TFY8bR0eCg7RwvIRYY6px8knCgjyIvSSKP4,1602
|
|
2
2
|
ai_pipeline_core/exceptions.py,sha256=_vW0Hbw2LGb5tcVvH0YzTKMff7QOPfCRr3w-w_zPyCE,968
|
|
3
|
+
ai_pipeline_core/pipeline.py,sha256=GOrPC53j756Xhpg_CShnkAKxSdkC16XHEoPeIhkjLIA,16569
|
|
4
|
+
ai_pipeline_core/prefect.py,sha256=VHYkkRcUmSpdwyWosOOxuExVCncIQgT6MypqGdjcYnM,241
|
|
3
5
|
ai_pipeline_core/prompt_manager.py,sha256=XmNUdMIC0WrE9fF0LIcfozAKOGrlYwj8AfXvCndIH-o,4693
|
|
4
6
|
ai_pipeline_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
7
|
ai_pipeline_core/settings.py,sha256=Zl2BPa6IHzh-B5V7cg5mtySr1dhWZQYYKxXz3BwrHlQ,615
|
|
6
|
-
ai_pipeline_core/tracing.py,sha256=
|
|
7
|
-
ai_pipeline_core/documents/__init__.py,sha256=
|
|
8
|
+
ai_pipeline_core/tracing.py,sha256=T-3fTyA37TejXxotkVzTNqL2a5nOfZ0bcHg9TClLvmg,9471
|
|
9
|
+
ai_pipeline_core/documents/__init__.py,sha256=TLW8eOEmthfDHOTssXjyBlqhgrZe9ZIyxlkd0LBJ3_s,340
|
|
8
10
|
ai_pipeline_core/documents/document.py,sha256=e3IBr0TThucBAaOHvdqv0X--iCcBrqh2jzFTyaOp7O0,12418
|
|
9
11
|
ai_pipeline_core/documents/document_list.py,sha256=HOG_uZDazA9CJB7Lr_tNcDFzb5Ff9RUt0ELWQK_eYNM,4940
|
|
10
12
|
ai_pipeline_core/documents/flow_document.py,sha256=qsV-2JYOMhkvAj7lW54ZNH_4QUclld9h06CoU59tWww,815
|
|
11
13
|
ai_pipeline_core/documents/mime_type.py,sha256=sBhNRoBJQ35JoHWhJzBGpp00WFDfMdEX0JZKKkR7QH0,3371
|
|
12
14
|
ai_pipeline_core/documents/task_document.py,sha256=WjHqtl1d60XFBBqewNRdz1OqBErGI0jRx15oQYCTHo8,907
|
|
13
15
|
ai_pipeline_core/documents/utils.py,sha256=BdE4taSl1vrBhxnFbOP5nDA7lXIcvY__AMRTHoaNb5M,2764
|
|
14
|
-
ai_pipeline_core/flow/__init__.py,sha256=
|
|
16
|
+
ai_pipeline_core/flow/__init__.py,sha256=54DRfZnjXQVrimgtKEVEm5u5ErImx31cjK2PpBvHjU4,116
|
|
15
17
|
ai_pipeline_core/flow/config.py,sha256=crbe_OvNE6qulIKv1D8yKoe8xrEsIlvICyxjhqHHBxQ,2266
|
|
18
|
+
ai_pipeline_core/flow/options.py,sha256=WygJEwjqOa14l23a_Hp36hJX-WgxHMq-YzSieC31Z4Y,701
|
|
16
19
|
ai_pipeline_core/llm/__init__.py,sha256=3XVK-bSJdOe0s6KmmO7PDbsXHfjlcZEG1MVBmaz3EeU,442
|
|
17
20
|
ai_pipeline_core/llm/ai_messages.py,sha256=DwJJe05BtYdnMZeHbBbyEbDCqrW63SRvprxptoJUCn4,4586
|
|
18
|
-
ai_pipeline_core/llm/client.py,sha256=
|
|
21
|
+
ai_pipeline_core/llm/client.py,sha256=VMs1nQKCfoxbcvE2mypn5QF19u90Ua87-5IiZxWOj98,7784
|
|
19
22
|
ai_pipeline_core/llm/model_options.py,sha256=TvAAlDFZN-TP9-J-RZBuU_dpSocskf6paaQMw1XY9UE,1321
|
|
20
23
|
ai_pipeline_core/llm/model_response.py,sha256=fIWueaemgo0cMruvToMZyKsRPzKwL6IlvUJN7DLG710,5558
|
|
21
24
|
ai_pipeline_core/llm/model_types.py,sha256=rIwY6voT8-xdfsKPDC0Gkdl2iTp9Q2LuvWGSRU9Mp3k,342
|
|
@@ -23,7 +26,10 @@ ai_pipeline_core/logging/__init__.py,sha256=DOO6ckgnMVXl29Sy7q6jhO-iW96h54pCHQDz
|
|
|
23
26
|
ai_pipeline_core/logging/logging.yml,sha256=YTW48keO_K5bkkb-KXGM7ZuaYKiquLsjsURei8Ql0V4,1353
|
|
24
27
|
ai_pipeline_core/logging/logging_config.py,sha256=6MBz9nnVNvqiLDoyy9-R3sWkn6927Re5hdz4hwTptpI,4903
|
|
25
28
|
ai_pipeline_core/logging/logging_mixin.py,sha256=RDaR2ju2-vKTJRzXGa0DquGPT8_UxahWjvKJnaD0IV8,7810
|
|
26
|
-
ai_pipeline_core
|
|
27
|
-
ai_pipeline_core
|
|
28
|
-
ai_pipeline_core
|
|
29
|
-
ai_pipeline_core-0.1.
|
|
29
|
+
ai_pipeline_core/simple_runner/__init__.py,sha256=OPbTCZvqpnYdwi1Knnkj-MpmD0Nvtg5O7UwIdAKz_AY,384
|
|
30
|
+
ai_pipeline_core/simple_runner/cli.py,sha256=TjiSh7lr1VnTbO1jA2DuVzC2AA6V_5sA5Z8XSuldQmc,3054
|
|
31
|
+
ai_pipeline_core/simple_runner/simple_runner.py,sha256=70BHT1iz-G368H2t4tsWAVni0jw2VkWVdnKICuVtLPw,5009
|
|
32
|
+
ai_pipeline_core-0.1.7.dist-info/METADATA,sha256=2Pi815TCTBlKnTp2duTaUJiKaextafqZ5yfPZdD_--o,18361
|
|
33
|
+
ai_pipeline_core-0.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
34
|
+
ai_pipeline_core-0.1.7.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
|
|
35
|
+
ai_pipeline_core-0.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|