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.
@@ -1,7 +1,23 @@
1
1
  """Pipeline Core - Shared infrastructure for AI pipelines."""
2
2
 
3
- from .documents import Document, DocumentList, FlowDocument, TaskDocument
4
- from .flow import FlowConfig
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
- get_pipeline_logger as get_logger,
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.6"
35
+ __version__ = "0.1.7"
20
36
 
21
37
  __all__ = [
22
- "Document",
23
- "DocumentList",
24
- "FlowConfig",
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
  ]
@@ -1,3 +1,7 @@
1
1
  from .config import FlowConfig
2
+ from .options import FlowOptions
2
3
 
3
- __all__ = ["FlowConfig"]
4
+ __all__ = [
5
+ "FlowConfig",
6
+ "FlowOptions",
7
+ ]
@@ -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"]
@@ -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,7 @@
1
+ """Prefect core features."""
2
+
3
+ from prefect import flow, task
4
+ from prefect.logging import disable_run_logger
5
+ from prefect.testing.utilities import prefect_test_harness
6
+
7
+ __all__ = ["task", "flow", "disable_run_logger", "prefect_test_harness"]
@@ -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
@@ -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="...", test=True)
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
- test: bool = False,
80
- debug_only: bool = False,
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
- test: bool = False,
100
- debug_only: bool = False,
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
- test: Mark this trace as a test run
114
- debug_only: Only trace when LMNR_DEBUG=true environment variable is set
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.6
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.5
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
- ### Prefect Flow Integration
154
+ ### Enhanced Pipeline Decorators (New in v0.1.7)
155
155
  ```python
156
- from prefect import flow, task
157
- from ai_pipeline_core.documents import Document, DocumentList, FlowDocument
158
- from ai_pipeline_core.flow import FlowConfig
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 OutputDocument(FlowDocument):
162
- """Custom output document type"""
163
- def get_type(self) -> str:
164
- return "output"
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
- class MyFlowConfig(FlowConfig):
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-level processing with automatic tracing
167
+ # Task with automatic tracing and retries
174
168
  result = await process_document(doc)
175
- # Convert result to JSON string for document content
176
- import json
177
- return OutputDocument(name="result", content=json.dumps(result.model_dump()).encode())
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
- @flow
180
- async def my_pipeline(documents: DocumentList):
181
- config = MyFlowConfig()
182
- input_docs = config.get_input_documents(documents)
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
- results = await process_task.map(input_docs)
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
- config.validate_output_documents(results)
187
- return results
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
- └── config.py # Type-safe flow configuration
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.6
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=2zVmBkQNdYcy2mQmw8qO1et3a5pv6KMTXfxrcTfA7EM,779
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=_bijptKWXh7V_xENFQGF11-B70rOGwV6g0qdBoF-VCw,7890
7
- ai_pipeline_core/documents/__init__.py,sha256=rEnKj-sSlZ9WnFlZAmSGVi1P8vnsHmU9O9_YwtP40ms,242
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=_Sji2yY1ICkvVX6QiiGWKzqIXtg9UAiuvhjHSK_gdO8,57
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=IOcyjwyAKQWlqnwC5p2Hl4FeRCzOJAHC5Yqr_oCBQ8s,7703
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-0.1.6.dist-info/METADATA,sha256=BSLr818JTSrsTGPjOEB7bQoCv4q3ep-0YX55UgJRH4s,15869
27
- ai_pipeline_core-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- ai_pipeline_core-0.1.6.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
29
- ai_pipeline_core-0.1.6.dist-info/RECORD,,
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,,