ai-pipeline-core 0.1.11__py3-none-any.whl → 0.1.12__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.
@@ -113,7 +113,7 @@ from .prompt_manager import PromptManager
113
113
  from .settings import Settings
114
114
  from .tracing import TraceInfo, TraceLevel, trace
115
115
 
116
- __version__ = "0.1.11"
116
+ __version__ = "0.1.12"
117
117
 
118
118
  __all__ = [
119
119
  # Config/Settings
@@ -156,7 +156,7 @@ class Document(BaseModel, ABC):
156
156
  DESCRIPTION_EXTENSION: ClassVar[str] = ".description.md"
157
157
  """File extension for description files."""
158
158
 
159
- MARKDOWN_LIST_SEPARATOR: ClassVar[str] = "\n\n---\n\n"
159
+ MARKDOWN_LIST_SEPARATOR: ClassVar[str] = "\n\n-----------------\n\n"
160
160
  """Separator for markdown list items."""
161
161
 
162
162
  def __init_subclass__(cls, **kwargs: Any) -> None:
@@ -254,7 +254,8 @@ class Document(BaseModel, ABC):
254
254
  - bytes: Used directly without conversion
255
255
  - str: Encoded to UTF-8 bytes
256
256
  - dict[str, Any]: Serialized to JSON (.json) or YAML (.yaml/.yml)
257
- - list[str]: Joined with separator for .md, else JSON/YAML
257
+ - list[str]: Joined with separator for .md (validates no items
258
+ contain separator), else JSON/YAML
258
259
  - list[BaseModel]: Serialized to JSON or YAML based on extension
259
260
  - BaseModel: Serialized to JSON or YAML based on extension
260
261
  description: Optional description - USUALLY OMIT THIS (defaults to None).
@@ -264,7 +265,8 @@ class Document(BaseModel, ABC):
264
265
  New Document instance with content converted to bytes
265
266
 
266
267
  Raises:
267
- ValueError: If content type is not supported for the file extension
268
+ ValueError: If content type is not supported for the file extension,
269
+ or if markdown list items contain the separator
268
270
  DocumentNameError: If filename violates validation rules
269
271
  DocumentSizeError: If content exceeds MAX_CONTENT_SIZE
270
272
 
@@ -573,7 +575,7 @@ class Document(BaseModel, ABC):
573
575
  2. str → UTF-8 encoding
574
576
  3. dict/BaseModel + .json → JSON serialization (indented)
575
577
  4. dict/BaseModel + .yaml/.yml → YAML serialization
576
- 5. list[str] + .md → Join with markdown separator
578
+ 5. list[str] + .md → Join with markdown separator (validates no items contain separator)
577
579
  6. list[Any] + .json/.yaml → JSON/YAML array
578
580
  7. int/float/bool + .json → JSON primitive
579
581
 
@@ -622,6 +624,13 @@ class Document(BaseModel, ABC):
622
624
  if name_lower.endswith(".md"):
623
625
  # For markdown files, join with separator
624
626
  if all(isinstance(item, str) for item in v):
627
+ # Check that no string contains the separator
628
+ for item in v:
629
+ if cls.MARKDOWN_LIST_SEPARATOR in item:
630
+ raise ValueError(
631
+ f"Markdown list item cannot contain the separator "
632
+ f"'{cls.MARKDOWN_LIST_SEPARATOR}' as it will mess up formatting"
633
+ )
625
634
  v = cls.MARKDOWN_LIST_SEPARATOR.join(v).encode("utf-8")
626
635
  else:
627
636
  raise ValueError(
@@ -1060,7 +1069,7 @@ class Document(BaseModel, ABC):
1060
1069
 
1061
1070
  @public
1062
1071
 
1063
- Splits text content using markdown separator ("\n\n---\n\n").
1072
+ Splits text content using markdown separator ("\n\n-----------------\n\n").
1064
1073
  Designed for markdown documents with multiple sections.
1065
1074
 
1066
1075
  Returns:
@@ -1076,7 +1085,7 @@ class Document(BaseModel, ABC):
1076
1085
  >>> doc.as_markdown_list() # Returns original sections
1077
1086
 
1078
1087
  >>> # Manual creation with separator
1079
- >>> content = "Part 1\n\n---\n\nPart 2\n\n---\n\nPart 3"
1088
+ >>> content = "Part 1\n\n-----------------\n\nPart 2\n\n-----------------\n\nPart 3"
1080
1089
  >>> doc2 = MyDocument(name="parts.md", content=content.encode())
1081
1090
  >>> doc2.as_markdown_list() # ['Part 1', 'Part 2', 'Part 3']
1082
1091
  """
@@ -11,7 +11,7 @@ Best Practice:
11
11
  """
12
12
 
13
13
  from abc import ABC
14
- from typing import Any, ClassVar
14
+ from typing import Any, ClassVar, Iterable
15
15
 
16
16
  from ai_pipeline_core.documents import DocumentList, FlowDocument
17
17
  from ai_pipeline_core.exceptions import DocumentValidationError
@@ -267,7 +267,7 @@ class FlowConfig(ABC):
267
267
 
268
268
  @classmethod
269
269
  def create_and_validate_output(
270
- cls, output: FlowDocument | list[FlowDocument] | DocumentList
270
+ cls, output: FlowDocument | Iterable[FlowDocument] | DocumentList
271
271
  ) -> DocumentList:
272
272
  """Create and validate flow output documents.
273
273
 
@@ -280,7 +280,7 @@ class FlowConfig(ABC):
280
280
  and validates it matches the expected OUTPUT_DOCUMENT_TYPE.
281
281
 
282
282
  Args:
283
- output: Single document, list of documents, or DocumentList.
283
+ output: Single document, iterable of documents, or DocumentList.
284
284
 
285
285
  Returns:
286
286
  Validated DocumentList containing the output documents.
@@ -308,7 +308,7 @@ class FlowConfig(ABC):
308
308
  elif isinstance(output, DocumentList):
309
309
  documents = output
310
310
  else:
311
- assert isinstance(output, list)
312
- documents = DocumentList(output) # type: ignore[arg-type]
311
+ # Handle any iterable of FlowDocuments
312
+ documents = DocumentList(list(output)) # type: ignore[arg-type]
313
313
  cls.validate_output_documents(documents)
314
314
  return documents
@@ -177,6 +177,38 @@ def _callable_name(obj: Any, fallback: str) -> str:
177
177
  return fallback
178
178
 
179
179
 
180
+ def _is_already_traced(func: Callable[..., Any]) -> bool:
181
+ """Check if a function has already been wrapped by the trace decorator.
182
+
183
+ This checks both for the explicit __is_traced__ marker and walks
184
+ the __wrapped__ chain to detect nested trace decorations.
185
+
186
+ Args:
187
+ func: Function to check for existing trace decoration.
188
+
189
+ Returns:
190
+ True if the function is already traced, False otherwise.
191
+ """
192
+ # Check for explicit marker
193
+ if hasattr(func, "__is_traced__") and func.__is_traced__: # type: ignore[attr-defined]
194
+ return True
195
+
196
+ # Walk the __wrapped__ chain to detect nested traces
197
+ current = func
198
+ depth = 0
199
+ max_depth = 10 # Prevent infinite loops
200
+
201
+ while hasattr(current, "__wrapped__") and depth < max_depth:
202
+ wrapped = current.__wrapped__ # type: ignore[attr-defined]
203
+ # Check if the wrapped function has the trace marker
204
+ if hasattr(wrapped, "__is_traced__") and wrapped.__is_traced__: # type: ignore[attr-defined]
205
+ return True
206
+ current = wrapped
207
+ depth += 1
208
+
209
+ return False
210
+
211
+
180
212
  # --------------------------------------------------------------------------- #
181
213
  # @pipeline_task — async-only, traced, returns Prefect's Task object
182
214
  # --------------------------------------------------------------------------- #
@@ -264,6 +296,9 @@ def pipeline_task(
264
296
  Wraps an async function with both Prefect task functionality and
265
297
  LMNR tracing. The function MUST be async (declared with 'async def').
266
298
 
299
+ IMPORTANT: Never combine with @trace decorator - this includes tracing automatically.
300
+ The framework will raise TypeError if you try to use both decorators together.
301
+
267
302
  Best Practice - Use Defaults:
268
303
  For 90% of use cases, use this decorator WITHOUT any parameters.
269
304
  Only specify parameters when you have EXPLICIT requirements.
@@ -354,13 +389,21 @@ def pipeline_task(
354
389
  Wrapped task with tracing and Prefect functionality.
355
390
 
356
391
  Raises:
357
- TypeError: If function is not async.
392
+ TypeError: If function is not async or already traced.
358
393
  """
359
394
  if not inspect.iscoroutinefunction(fn):
360
395
  raise TypeError(
361
396
  f"@pipeline_task target '{_callable_name(fn, 'task')}' must be 'async def'"
362
397
  )
363
398
 
399
+ # Check if function is already traced
400
+ if _is_already_traced(fn):
401
+ raise TypeError(
402
+ f"@pipeline_task target '{_callable_name(fn, 'task')}' is already decorated "
403
+ f"with @trace. Remove the @trace decorator - @pipeline_task includes "
404
+ f"tracing automatically."
405
+ )
406
+
364
407
  fname = _callable_name(fn, "task")
365
408
  traced_fn = trace(
366
409
  level=trace_level,
@@ -482,6 +525,9 @@ def pipeline_flow(
482
525
  Wraps an async function as a Prefect flow with tracing and type safety.
483
526
  The decorated function MUST be async and follow the required signature.
484
527
 
528
+ IMPORTANT: Never combine with @trace decorator - this includes tracing automatically.
529
+ The framework will raise TypeError if you try to use both decorators together.
530
+
485
531
  Best Practice - Use Defaults:
486
532
  For 90% of use cases, use this decorator WITHOUT any parameters.
487
533
  Only specify parameters when you have EXPLICIT requirements.
@@ -590,13 +636,22 @@ def pipeline_flow(
590
636
  Wrapped flow with tracing and Prefect functionality.
591
637
 
592
638
  Raises:
593
- TypeError: If function is not async, doesn't have required
594
- parameters, or doesn't return DocumentList.
639
+ TypeError: If function is not async, already traced, doesn't have
640
+ required parameters, or doesn't return DocumentList.
595
641
  """
596
642
  fname = _callable_name(fn, "flow")
597
643
 
598
644
  if not inspect.iscoroutinefunction(fn):
599
645
  raise TypeError(f"@pipeline_flow '{fname}' must be declared with 'async def'")
646
+
647
+ # Check if function is already traced
648
+ if _is_already_traced(fn):
649
+ raise TypeError(
650
+ f"@pipeline_flow target '{fname}' is already decorated "
651
+ f"with @trace. Remove the @trace decorator - @pipeline_flow includes "
652
+ f"tracing automatically."
653
+ )
654
+
600
655
  if len(inspect.signature(fn).parameters) < 3:
601
656
  raise TypeError(
602
657
  f"@pipeline_flow '{fname}' must accept "
@@ -336,7 +336,25 @@ def trace(
336
336
 
337
337
  Returns:
338
338
  Wrapped function with LMNR observability.
339
+
340
+ Raises:
341
+ TypeError: If function is already decorated with @pipeline_task or @pipeline_flow.
339
342
  """
343
+ # Check if this is already a traced pipeline_task or pipeline_flow
344
+ # This happens when @trace is applied after @pipeline_task/@pipeline_flow
345
+ if hasattr(f, "__is_traced__") and f.__is_traced__: # type: ignore[attr-defined]
346
+ # Check if it's a Prefect Task or Flow object (they have specific attributes)
347
+ # Prefect objects have certain attributes that regular functions don't
348
+ is_prefect_task = hasattr(f, "fn") and hasattr(f, "submit") and hasattr(f, "map")
349
+ is_prefect_flow = hasattr(f, "fn") and hasattr(f, "serve")
350
+ if is_prefect_task or is_prefect_flow:
351
+ fname = getattr(f, "__name__", "function")
352
+ raise TypeError(
353
+ f"Function '{fname}' is already decorated with @pipeline_task or "
354
+ f"@pipeline_flow. Remove the @trace decorator - pipeline decorators "
355
+ f"include tracing automatically."
356
+ )
357
+
340
358
  # Handle 'debug' level logic - only trace when LMNR_DEBUG is "true"
341
359
  if level == "debug" and os.getenv("LMNR_DEBUG", "").lower() != "true":
342
360
  return f
@@ -437,6 +455,9 @@ def trace(
437
455
 
438
456
  wrapper = async_wrapper if is_coroutine else sync_wrapper
439
457
 
458
+ # Mark function as traced for detection by pipeline decorators
459
+ wrapper.__is_traced__ = True # type: ignore[attr-defined]
460
+
440
461
  # Preserve the original function signature
441
462
  try:
442
463
  wrapper.__signature__ = sig # type: ignore[attr-defined]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-pipeline-core
3
- Version: 0.1.11
3
+ Version: 0.1.12
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
@@ -1,13 +1,13 @@
1
- ai_pipeline_core/__init__.py,sha256=kq6f5Xzd7UeZQUgo4uzJoFCl3MC0A2mgQJrGuLTsufk,5396
1
+ ai_pipeline_core/__init__.py,sha256=woPhgoXf2GlKkWqvzBmf57ODLMsmsoInB0nimFrsuzE,5396
2
2
  ai_pipeline_core/exceptions.py,sha256=vx-XLTw2fJSPs-vwtXVYtqoQUcOc0JeI7UmHqRqQYWU,1569
3
- ai_pipeline_core/pipeline.py,sha256=0yQLuFaaBipujfZPDI-P-c7irMcszW1p8ss6yYvjVj0,25986
3
+ ai_pipeline_core/pipeline.py,sha256=EETCEoKkFcVvsQqtVIV5S5DrVII4XPSuCnqF7zA2_jc,28137
4
4
  ai_pipeline_core/prefect.py,sha256=CC8qeIpVqzNq8m6YWNIcRYeDEqkcAFiNjFwcuwwKO0k,2064
5
5
  ai_pipeline_core/prompt_manager.py,sha256=XZwah5fp3GyZ0e0na_yOs6m4ngCcotysh-K_cU2U978,11572
6
6
  ai_pipeline_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  ai_pipeline_core/settings.py,sha256=XfIw-UOuelSiPAYCd3VmOhsGmAxsCy9IsxWPWAf0wGY,4427
8
- ai_pipeline_core/tracing.py,sha256=seGnOEQydUBzCBZI4C-hgzF_1t5Yzw6DurjlODQ5POU,17119
8
+ ai_pipeline_core/tracing.py,sha256=75wKL__0arY54dnDcG3F-4KC6mDXg6lMA219-fpxhjI,18348
9
9
  ai_pipeline_core/documents/__init__.py,sha256=FOYBUKipW_uuzFieW3MigvNLEpbCI1jeY9_0VxJsoS0,692
10
- ai_pipeline_core/documents/document.py,sha256=bpfBDvYsgX5MBKzLogonRXT7MIT43SX0jju-TarQoio,50865
10
+ ai_pipeline_core/documents/document.py,sha256=tDUBootN_hK75k3XASyBxt-ZqtZuynTwgcL_L5ZWN1Q,51521
11
11
  ai_pipeline_core/documents/document_list.py,sha256=m8ei2jLTHt47uCKHmn8BuMMbfwPIKH5g9oPO1jvIPmg,8282
12
12
  ai_pipeline_core/documents/flow_document.py,sha256=bJxLjvUE6xfR5-JCy1oi7XtZOwLBOSuUBBJjRaQk6TI,4748
13
13
  ai_pipeline_core/documents/mime_type.py,sha256=DkW88K95el5nAmhC00XLS0G3WpDXgs5IRsBWbKiqG3Y,7995
@@ -15,7 +15,7 @@ ai_pipeline_core/documents/task_document.py,sha256=HonlS7hO6zisu67JFXu6LjGLyc7Bu
15
15
  ai_pipeline_core/documents/temporary_document.py,sha256=cMuNLnRlWrMryzfZXOIrTrZ2blsxjmflnL8O3zicWIk,3240
16
16
  ai_pipeline_core/documents/utils.py,sha256=VHq1gAWTMmJNS55HSZ2--sDErE4DVtFsxrv6y2KXmdk,3472
17
17
  ai_pipeline_core/flow/__init__.py,sha256=2BfWYMOPYW5teGzwo-qzpn_bom1lxxry0bPsjVgcsCk,188
18
- ai_pipeline_core/flow/config.py,sha256=ZaTEyaGxPe2JUSlaX7tY0BsMyK4FMtSAmFu5CkzQTOM,12083
18
+ ai_pipeline_core/flow/config.py,sha256=4JVc30tztSW9sYufWLN3hx6qSeR1VX31H1aI9I2jIrA,12114
19
19
  ai_pipeline_core/flow/options.py,sha256=UiddkxWrXAhsFtOOt06JhttTp-0gejc8kG0K8Falg1c,2297
20
20
  ai_pipeline_core/llm/__init__.py,sha256=QWhMVs4OLTgIvOHfxb7AQhpfCXlgGpoGiJ8PSbVyzZs,661
21
21
  ai_pipeline_core/llm/ai_messages.py,sha256=YS3tfqivj9kS6sYsDAgw9LHEjg9cKS1SAHviCzkvAos,8492
@@ -30,7 +30,7 @@ ai_pipeline_core/logging/logging_mixin.py,sha256=UFd_CfyJ6YP_XVA-CrpAszOr8g1FH8R
30
30
  ai_pipeline_core/simple_runner/__init__.py,sha256=OXKFOu3rRcqXCWwBBxnZ7Vz8KRFF5g-G3eJq-vm3CUY,521
31
31
  ai_pipeline_core/simple_runner/cli.py,sha256=sbIvv_d401o8h-b5JlcIJQhwzte1sttdmUi2a3As-wY,9357
32
32
  ai_pipeline_core/simple_runner/simple_runner.py,sha256=Q7PRAgf_VUPS72WV9pER9WT0AQo9r4WbRclsRXXJiW4,14329
33
- ai_pipeline_core-0.1.11.dist-info/METADATA,sha256=zPo-FixIjsW5ghdGNHcrM7LZtkSX4Rp4S5EeAfIIi5Q,13192
34
- ai_pipeline_core-0.1.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
- ai_pipeline_core-0.1.11.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
36
- ai_pipeline_core-0.1.11.dist-info/RECORD,,
33
+ ai_pipeline_core-0.1.12.dist-info/METADATA,sha256=ytaOH7I8CR_jRrRvvZW8HDldFdFw8mT8gYC5G-X-NRA,13192
34
+ ai_pipeline_core-0.1.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ ai_pipeline_core-0.1.12.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
36
+ ai_pipeline_core-0.1.12.dist-info/RECORD,,