ai-pipeline-core 0.2.0__py3-none-any.whl → 0.2.2__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.
@@ -118,7 +118,7 @@ from .prompt_manager import PromptManager
118
118
  from .settings import Settings
119
119
  from .tracing import TraceInfo, TraceLevel, set_trace_cost, trace
120
120
 
121
- __version__ = "0.2.0"
121
+ __version__ = "0.2.2"
122
122
 
123
123
  __all__ = [
124
124
  # Config/Settings
@@ -51,6 +51,7 @@ from .mime_type import (
51
51
  )
52
52
 
53
53
  TModel = TypeVar("TModel", bound=BaseModel)
54
+ TDocument = TypeVar("TDocument", bound="Document")
54
55
 
55
56
 
56
57
  class Document(BaseModel, ABC):
@@ -97,6 +98,8 @@ class Document(BaseModel, ABC):
97
98
  - Support for text, JSON, YAML, PDF, and image formats
98
99
  - Conversion utilities between different formats
99
100
  - Source provenance tracking via sources field
101
+ - Document type conversion via model_convert() method
102
+ - Standard Pydantic model_copy() for same-type copying
100
103
 
101
104
  Class Variables:
102
105
  MAX_CONTENT_SIZE: Maximum allowed content size in bytes (default 25MB)
@@ -223,6 +226,14 @@ class Document(BaseModel, ABC):
223
226
  ... sources=[source_doc.sha256] # Reference source document
224
227
  ... )
225
228
  >>> processed.has_source(source_doc) # True
229
+ >>>
230
+ >>> # Document copying and type conversion:
231
+ >>> # Standard Pydantic model_copy (doesn't validate updates)
232
+ >>> copied = doc.model_copy(update={"name": "new_name.json"})
233
+ >>> # Type conversion with validation via model_convert
234
+ >>> task_doc = MyTaskDoc.create(name="temp.json", content={"data": "value"})
235
+ >>> flow_doc = task_doc.model_convert(MyFlowDoc) # Convert to FlowDocument
236
+ >>> flow_doc.is_flow # True
226
237
  """
227
238
 
228
239
  MAX_CONTENT_SIZE: ClassVar[int] = 25 * 1024 * 1024
@@ -1498,6 +1509,8 @@ class Document(BaseModel, ABC):
1498
1509
  - sha256: Full SHA256 hash in base32 encoding without padding (str)
1499
1510
  - mime_type: Detected MIME type (str)
1500
1511
  - sources: List of source strings (list[dict])
1512
+ - canonical_name: Canonical snake_case name for debug tracing (str)
1513
+ - class_name: Name of the actual document class for debug tracing (str)
1501
1514
  - content: Encoded content (str)
1502
1515
  - content_encoding: Either "utf-8" or "base64" (str)
1503
1516
 
@@ -1521,10 +1534,12 @@ class Document(BaseModel, ABC):
1521
1534
  "sha256": self.sha256,
1522
1535
  "mime_type": self.mime_type,
1523
1536
  "sources": self.sources,
1537
+ "canonical_name": canonical_name_key(self.__class__),
1538
+ "class_name": self.__class__.__name__,
1524
1539
  }
1525
1540
 
1526
1541
  # Try to encode content as UTF-8, fall back to base64
1527
- if self.is_text or self.mime_type.startswith("text/"):
1542
+ if self.is_text:
1528
1543
  try:
1529
1544
  result["content"] = self.content.decode("utf-8")
1530
1545
  result["content_encoding"] = "utf-8"
@@ -1600,3 +1615,96 @@ class Document(BaseModel, ABC):
1600
1615
  description=data.get("description"),
1601
1616
  sources=data.get("sources", []),
1602
1617
  )
1618
+
1619
+ @final
1620
+ def model_convert(
1621
+ self,
1622
+ new_type: type[TDocument],
1623
+ *,
1624
+ update: dict[str, Any] | None = None,
1625
+ deep: bool = False,
1626
+ ) -> TDocument:
1627
+ """Convert document to a different Document type with optional updates.
1628
+
1629
+ @public
1630
+
1631
+ Creates a new document of a different type, preserving all attributes
1632
+ while allowing updates. This is useful for converting between document
1633
+ types (e.g., TaskDocument to FlowDocument) while maintaining data integrity.
1634
+
1635
+ Args:
1636
+ new_type: Target Document class for conversion. Must be a concrete
1637
+ subclass of Document (not abstract classes like Document,
1638
+ FlowDocument, or TaskDocument).
1639
+ update: Dictionary of attributes to update. Supports any attributes
1640
+ that the Document constructor accepts (name, content,
1641
+ description, sources).
1642
+ deep: Whether to perform a deep copy of mutable attributes.
1643
+
1644
+ Returns:
1645
+ New Document instance of the specified type.
1646
+
1647
+ Raises:
1648
+ TypeError: If new_type is not a subclass of Document, is an abstract
1649
+ class, or if update contains invalid attributes.
1650
+ DocumentNameError: If the name violates the target type's FILES enum.
1651
+ DocumentSizeError: If content exceeds MAX_CONTENT_SIZE.
1652
+
1653
+ Example:
1654
+ >>> # Convert TaskDocument to FlowDocument
1655
+ >>> task_doc = MyTaskDoc.create(name="temp.json", content={"data": "value"})
1656
+ >>> flow_doc = task_doc.model_convert(MyFlowDoc)
1657
+ >>> assert flow_doc.is_flow
1658
+ >>> assert flow_doc.content == task_doc.content
1659
+ >>>
1660
+ >>> # Convert with updates
1661
+ >>> updated = task_doc.model_convert(
1662
+ ... MyFlowDoc,
1663
+ ... update={"name": "permanent.json", "description": "Converted"}
1664
+ ... )
1665
+ >>>
1666
+ >>> # Track document lineage
1667
+ >>> derived = doc.model_convert(
1668
+ ... ProcessedDoc,
1669
+ ... update={"sources": [doc.sha256]}
1670
+ ... )
1671
+ """
1672
+ # Validate new_type
1673
+ try:
1674
+ # Use a runtime check to ensure it's a class
1675
+ if not isinstance(new_type, type): # type: ignore[reportIncompatibleArgumentType]
1676
+ raise TypeError(f"new_type must be a class, got {new_type}")
1677
+ if not issubclass(new_type, Document): # type: ignore[reportIncompatibleArgumentType]
1678
+ raise TypeError(f"new_type must be a subclass of Document, got {new_type}")
1679
+ except (TypeError, AttributeError):
1680
+ # Not a class at all
1681
+ raise TypeError(f"new_type must be a subclass of Document, got {new_type}")
1682
+
1683
+ # Check for abstract classes by name (avoid circular imports)
1684
+ class_name = new_type.__name__
1685
+ if class_name == "Document":
1686
+ raise TypeError("Cannot instantiate abstract Document class directly")
1687
+ if class_name == "FlowDocument":
1688
+ raise TypeError("Cannot instantiate abstract FlowDocument class directly")
1689
+ if class_name == "TaskDocument":
1690
+ raise TypeError("Cannot instantiate abstract TaskDocument class directly")
1691
+
1692
+ # Get current document data with proper typing
1693
+ data: dict[str, Any] = {
1694
+ "name": self.name,
1695
+ "content": self.content,
1696
+ "description": self.description,
1697
+ "sources": self.sources.copy() if deep else self.sources,
1698
+ }
1699
+
1700
+ # Apply updates if provided
1701
+ if update:
1702
+ data.update(update)
1703
+
1704
+ # Create new document of target type
1705
+ return new_type(
1706
+ name=data["name"],
1707
+ content=data["content"],
1708
+ description=data.get("description"),
1709
+ sources=data.get("sources", []),
1710
+ )
@@ -3,7 +3,8 @@
3
3
  @public
4
4
  """
5
5
 
6
- from typing import Any, Iterable, SupportsIndex, Union, overload
6
+ from copy import deepcopy
7
+ from typing import Any, Callable, Iterable, SupportsIndex, Union, overload
7
8
 
8
9
  from typing_extensions import Self
9
10
 
@@ -37,6 +38,7 @@ class DocumentList(list[Document]):
37
38
  documents: list[Document] | None = None,
38
39
  validate_same_type: bool = False,
39
40
  validate_duplicates: bool = False,
41
+ frozen: bool = False,
40
42
  ) -> None:
41
43
  """Initialize DocumentList.
42
44
 
@@ -46,12 +48,15 @@ class DocumentList(list[Document]):
46
48
  documents: Initial list of documents.
47
49
  validate_same_type: Enforce same document type.
48
50
  validate_duplicates: Prevent duplicate filenames.
51
+ frozen: If True, list is immutable from creation.
49
52
  """
50
53
  super().__init__()
51
54
  self._validate_same_type = validate_same_type
52
55
  self._validate_duplicates = validate_duplicates
56
+ self._frozen = False # Initialize as unfrozen to allow initial population
53
57
  if documents:
54
58
  self.extend(documents)
59
+ self._frozen = frozen # Set frozen state after initial population
55
60
 
56
61
  def _validate_no_duplicates(self) -> None:
57
62
  """Check for duplicate document names.
@@ -109,18 +114,51 @@ class DocumentList(list[Document]):
109
114
  self._validate_no_description_files()
110
115
  self._validate_types()
111
116
 
117
+ def freeze(self) -> None:
118
+ """Permanently freeze the list, preventing modifications.
119
+
120
+ Once frozen, the list cannot be unfrozen.
121
+ """
122
+ self._frozen = True
123
+
124
+ def copy(self) -> "DocumentList":
125
+ """Create an unfrozen deep copy of the list.
126
+
127
+ Returns:
128
+ New unfrozen DocumentList with deep-copied documents.
129
+ """
130
+ copied_docs = deepcopy(list(self))
131
+ return DocumentList(
132
+ documents=copied_docs,
133
+ validate_same_type=self._validate_same_type,
134
+ validate_duplicates=self._validate_duplicates,
135
+ frozen=False, # Copies are always unfrozen
136
+ )
137
+
138
+ def _check_frozen(self) -> None:
139
+ """Check if list is frozen and raise if it is.
140
+
141
+ Raises:
142
+ RuntimeError: If the list is frozen.
143
+ """
144
+ if self._frozen:
145
+ raise RuntimeError("Cannot modify frozen DocumentList")
146
+
112
147
  def append(self, document: Document) -> None:
113
148
  """Add a document to the end of the list."""
149
+ self._check_frozen()
114
150
  super().append(document)
115
151
  self._validate()
116
152
 
117
153
  def extend(self, documents: Iterable[Document]) -> None:
118
154
  """Add multiple documents to the list."""
155
+ self._check_frozen()
119
156
  super().extend(documents)
120
157
  self._validate()
121
158
 
122
159
  def insert(self, index: SupportsIndex, document: Document) -> None:
123
160
  """Insert a document at the specified position."""
161
+ self._check_frozen()
124
162
  super().insert(index, document)
125
163
  self._validate()
126
164
 
@@ -132,6 +170,7 @@ class DocumentList(list[Document]):
132
170
 
133
171
  def __setitem__(self, index: Union[SupportsIndex, slice], value: Any) -> None:
134
172
  """Set item or slice with validation."""
173
+ self._check_frozen()
135
174
  super().__setitem__(index, value)
136
175
  self._validate()
137
176
 
@@ -141,10 +180,48 @@ class DocumentList(list[Document]):
141
180
  Returns:
142
181
  Self: This DocumentList after modification.
143
182
  """
183
+ self._check_frozen()
144
184
  result = super().__iadd__(other)
145
185
  self._validate()
146
186
  return result
147
187
 
188
+ def __delitem__(self, index: Union[SupportsIndex, slice]) -> None:
189
+ """Delete item or slice from list."""
190
+ self._check_frozen()
191
+ super().__delitem__(index)
192
+
193
+ def pop(self, index: SupportsIndex = -1) -> Document:
194
+ """Remove and return item at index.
195
+
196
+ Returns:
197
+ Document removed from the list.
198
+ """
199
+ self._check_frozen()
200
+ return super().pop(index)
201
+
202
+ def remove(self, document: Document) -> None:
203
+ """Remove first occurrence of document."""
204
+ self._check_frozen()
205
+ super().remove(document)
206
+
207
+ def clear(self) -> None:
208
+ """Remove all items from list."""
209
+ self._check_frozen()
210
+ super().clear()
211
+
212
+ def reverse(self) -> None:
213
+ """Reverse list in place."""
214
+ self._check_frozen()
215
+ super().reverse()
216
+
217
+ def sort(self, *, key: Callable[[Document], Any] | None = None, reverse: bool = False) -> None:
218
+ """Sort list in place."""
219
+ self._check_frozen()
220
+ if key is None:
221
+ super().sort(reverse=reverse) # type: ignore[call-arg]
222
+ else:
223
+ super().sort(key=key, reverse=reverse)
224
+
148
225
  @overload
149
226
  def filter_by(self, arg: str) -> "DocumentList": ...
150
227
 
@@ -8,8 +8,6 @@ from .ai_messages import AIMessages, AIMessageType
8
8
  from .client import (
9
9
  generate,
10
10
  generate_structured,
11
- generate_with_retry_for_testing,
12
- process_messages_for_testing,
13
11
  )
14
12
  from .model_options import ModelOptions
15
13
  from .model_response import ModelResponse, StructuredModelResponse
@@ -24,7 +22,4 @@ __all__ = [
24
22
  "StructuredModelResponse",
25
23
  "generate",
26
24
  "generate_structured",
27
- # Internal functions exposed for testing only
28
- "process_messages_for_testing",
29
- "generate_with_retry_for_testing",
30
25
  ]
@@ -9,6 +9,8 @@ including text, documents, and model responses.
9
9
  import base64
10
10
  import hashlib
11
11
  import json
12
+ from copy import deepcopy
13
+ from typing import Any, Callable, Iterable, SupportsIndex, Union
12
14
 
13
15
  from openai.types.chat import (
14
16
  ChatCompletionContentPartParam,
@@ -64,8 +66,8 @@ class AIMessages(list[AIMessageType]):
64
66
  CAUTION: AIMessages is a list subclass. Always use list construction (e.g.,
65
67
  `AIMessages(["text"])`) or empty constructor with append (e.g.,
66
68
  `AIMessages(); messages.append("text")`). Never pass raw strings directly to the
67
- constructor (`AIMessages("text")`) as this will iterate over the string characters
68
- instead of treating it as a single message.
69
+ constructor (`AIMessages("text")`) as this will raise a TypeError to prevent
70
+ accidental character iteration.
69
71
 
70
72
  Example:
71
73
  >>> from ai_pipeline_core import llm
@@ -75,6 +77,127 @@ class AIMessages(list[AIMessageType]):
75
77
  >>> messages.append(response) # Add the actual response
76
78
  """
77
79
 
80
+ def __init__(self, iterable: Iterable[AIMessageType] | None = None, *, frozen: bool = False):
81
+ """Initialize AIMessages with optional iterable.
82
+
83
+ Args:
84
+ iterable: Optional iterable of messages (list, tuple, etc.).
85
+ Must not be a string.
86
+ frozen: If True, list is immutable from creation.
87
+
88
+ Raises:
89
+ TypeError: If a string is passed directly to the constructor.
90
+ """
91
+ if isinstance(iterable, str):
92
+ raise TypeError(
93
+ "AIMessages cannot be constructed from a string directly. "
94
+ "Use AIMessages(['text']) for a single message or "
95
+ "AIMessages() and then append('text')."
96
+ )
97
+ self._frozen = False # Initialize as unfrozen to allow initial population
98
+ if iterable is None:
99
+ super().__init__()
100
+ else:
101
+ super().__init__(iterable)
102
+ self._frozen = frozen # Set frozen state after initial population
103
+
104
+ def freeze(self) -> None:
105
+ """Permanently freeze the list, preventing modifications.
106
+
107
+ Once frozen, the list cannot be unfrozen.
108
+ """
109
+ self._frozen = True
110
+
111
+ def copy(self) -> "AIMessages":
112
+ """Create an unfrozen deep copy of the list.
113
+
114
+ Returns:
115
+ New unfrozen AIMessages with deep-copied messages.
116
+ """
117
+ copied_messages = deepcopy(list(self))
118
+ return AIMessages(copied_messages, frozen=False)
119
+
120
+ def _check_frozen(self) -> None:
121
+ """Check if list is frozen and raise if it is.
122
+
123
+ Raises:
124
+ RuntimeError: If the list is frozen.
125
+ """
126
+ if self._frozen:
127
+ raise RuntimeError("Cannot modify frozen AIMessages")
128
+
129
+ def append(self, message: AIMessageType) -> None:
130
+ """Add a message to the end of the list."""
131
+ self._check_frozen()
132
+ super().append(message)
133
+
134
+ def extend(self, messages: Iterable[AIMessageType]) -> None:
135
+ """Add multiple messages to the list."""
136
+ self._check_frozen()
137
+ super().extend(messages)
138
+
139
+ def insert(self, index: SupportsIndex, message: AIMessageType) -> None:
140
+ """Insert a message at the specified position."""
141
+ self._check_frozen()
142
+ super().insert(index, message)
143
+
144
+ def __setitem__(
145
+ self,
146
+ index: Union[SupportsIndex, slice],
147
+ value: Union[AIMessageType, Iterable[AIMessageType]],
148
+ ) -> None:
149
+ """Set item or slice."""
150
+ self._check_frozen()
151
+ super().__setitem__(index, value) # type: ignore[arg-type]
152
+
153
+ def __iadd__(self, other: Iterable[AIMessageType]) -> "AIMessages":
154
+ """In-place addition (+=).
155
+
156
+ Returns:
157
+ This AIMessages instance after modification.
158
+ """
159
+ self._check_frozen()
160
+ return super().__iadd__(other)
161
+
162
+ def __delitem__(self, index: Union[SupportsIndex, slice]) -> None:
163
+ """Delete item or slice from list."""
164
+ self._check_frozen()
165
+ super().__delitem__(index)
166
+
167
+ def pop(self, index: SupportsIndex = -1) -> AIMessageType:
168
+ """Remove and return item at index.
169
+
170
+ Returns:
171
+ AIMessageType removed from the list.
172
+ """
173
+ self._check_frozen()
174
+ return super().pop(index)
175
+
176
+ def remove(self, message: AIMessageType) -> None:
177
+ """Remove first occurrence of message."""
178
+ self._check_frozen()
179
+ super().remove(message)
180
+
181
+ def clear(self) -> None:
182
+ """Remove all items from list."""
183
+ self._check_frozen()
184
+ super().clear()
185
+
186
+ def reverse(self) -> None:
187
+ """Reverse list in place."""
188
+ self._check_frozen()
189
+ super().reverse()
190
+
191
+ def sort(
192
+ self, *, key: Callable[[AIMessageType], Any] | None = None, reverse: bool = False
193
+ ) -> None:
194
+ """Sort list in place."""
195
+ self._check_frozen()
196
+ if key is None:
197
+ super().sort(reverse=reverse) # type: ignore[call-arg]
198
+ else:
199
+ super().sort(key=key, reverse=reverse)
200
+
78
201
  def get_last_message(self) -> AIMessageType:
79
202
  """Get the last message in the conversation.
80
203
 
@@ -37,7 +37,7 @@ def _process_messages(
37
37
  context: AIMessages,
38
38
  messages: AIMessages,
39
39
  system_prompt: str | None = None,
40
- cache_ttl: str | None = "120s",
40
+ cache_ttl: str | None = "5m",
41
41
  ) -> list[ChatCompletionMessageParam]:
42
42
  """Process and format messages for LLM API consumption.
43
43
 
@@ -245,7 +245,7 @@ async def generate(
245
245
  model: Model to use (e.g., "gpt-5", "gemini-2.5-pro", "grok-4").
246
246
  Accepts predefined models or any string for custom models.
247
247
  context: Static context to cache (documents, examples, instructions).
248
- Defaults to None (empty context). Cached for 120 seconds.
248
+ Defaults to None (empty context). Cached for 5 minutes by default.
249
249
  messages: Dynamic messages/queries. AIMessages or str ONLY.
250
250
  Do not pass Document or DocumentList directly.
251
251
  If string, converted to AIMessages internally.
@@ -338,13 +338,13 @@ async def generate(
338
338
  - Context caching saves ~50-90% tokens on repeated calls
339
339
  - First call: full token cost
340
340
  - Subsequent calls (within cache TTL): only messages tokens
341
- - Default cache TTL is 120s (production-optimized)
341
+ - Default cache TTL is 5m (production-optimized)
342
342
  - Default retry logic: 3 attempts with 10s delay (production-optimized)
343
343
 
344
344
  Caching:
345
345
  When enabled in your LiteLLM proxy and supported by the upstream provider,
346
346
  context messages may be cached to reduce token usage on repeated calls.
347
- Default TTL is 120s (optimized for production workloads). Configure caching
347
+ Default TTL is 5m (optimized for production workloads). Configure caching
348
348
  behavior centrally via your LiteLLM proxy settings, not per API call.
349
349
  Savings depend on provider and payload; treat this as an optimization, not a guarantee.
350
350
 
@@ -524,6 +524,8 @@ async def generate_structured(
524
524
  if isinstance(messages, str):
525
525
  messages = AIMessages([messages])
526
526
 
527
+ assert isinstance(messages, AIMessages)
528
+
527
529
  # Call the internal generate function with structured output enabled
528
530
  try:
529
531
  response = await _generate_with_retry(model, context, messages, options)
@@ -555,9 +557,3 @@ async def generate_structured(
555
557
 
556
558
  # Create a StructuredModelResponse with the parsed value
557
559
  return StructuredModelResponse[T](chat_completion=response, parsed_value=parsed_value)
558
-
559
-
560
- # Public aliases for testing internal functions
561
- # These are exported to allow testing of implementation details
562
- process_messages_for_testing = _process_messages
563
- generate_with_retry_for_testing = _generate_with_retry
@@ -45,7 +45,7 @@ class ModelOptions(BaseModel):
45
45
 
46
46
  timeout: Maximum seconds to wait for response (default: 300).
47
47
 
48
- cache_ttl: Cache TTL for context messages (default: "120s").
48
+ cache_ttl: Cache TTL for context messages (default: "5m").
49
49
  String format like "60s", "5m", or None to disable caching.
50
50
  Applied to the last context message for efficient token reuse.
51
51
 
@@ -109,7 +109,7 @@ class ModelOptions(BaseModel):
109
109
  - search_context_size only works with search models
110
110
  - reasoning_effort only works with models that support explicit reasoning
111
111
  - response_format is set internally by generate_structured()
112
- - cache_ttl accepts formats like "120s", "5m", "1h" or None to disable caching
112
+ - cache_ttl accepts formats like "120s", "5m" (default), "1h" or None to disable caching
113
113
  """
114
114
 
115
115
  temperature: float | None = None
@@ -118,11 +118,13 @@ class ModelOptions(BaseModel):
118
118
  reasoning_effort: Literal["low", "medium", "high"] | None = None
119
119
  retries: int = 3
120
120
  retry_delay_seconds: int = 10
121
- timeout: int = 300
122
- cache_ttl: str | None = "120s"
121
+ timeout: int = 600
122
+ cache_ttl: str | None = "5m"
123
123
  service_tier: Literal["auto", "default", "flex", "scale", "priority"] | None = None
124
124
  max_completion_tokens: int | None = None
125
125
  response_format: type[BaseModel] | None = None
126
+ verbosity: Literal["low", "medium", "high"] | None = None
127
+ usage_tracking: bool = True
126
128
 
127
129
  def to_openai_completion_kwargs(self) -> dict[str, Any]:
128
130
  """Convert options to OpenAI API completion parameters.
@@ -159,7 +161,7 @@ class ModelOptions(BaseModel):
159
161
  Note:
160
162
  - system_prompt is handled separately in _process_messages()
161
163
  - retries and retry_delay_seconds are used by retry logic
162
- - extra_body is always included for potential extensions
164
+ - extra_body always includes usage tracking for cost monitoring
163
165
  """
164
166
  kwargs: dict[str, Any] = {
165
167
  "timeout": self.timeout,
@@ -184,4 +186,10 @@ class ModelOptions(BaseModel):
184
186
  if self.service_tier:
185
187
  kwargs["service_tier"] = self.service_tier
186
188
 
189
+ if self.verbosity:
190
+ kwargs["verbosity"] = self.verbosity
191
+
192
+ if self.usage_tracking:
193
+ kwargs["extra_body"]["usage"] = {"include": True}
194
+
187
195
  return kwargs
@@ -110,7 +110,8 @@ class ModelResponse(ChatCompletion):
110
110
  >>> if "error" in response.content.lower():
111
111
  ... # Handle error case
112
112
  """
113
- return self.choices[0].message.content or ""
113
+ content = self.choices[0].message.content or ""
114
+ return content.split("</think>")[-1].strip()
114
115
 
115
116
  def set_model_options(self, options: dict[str, Any]) -> None:
116
117
  """Store the model configuration used for generation.
@@ -21,12 +21,12 @@ ModelName: TypeAlias = (
21
21
  # Small models
22
22
  "gemini-2.5-flash",
23
23
  "gpt-5-mini",
24
- "grok-3-mini",
24
+ "grok-4-fast",
25
25
  # Search models
26
26
  "gemini-2.5-flash-search",
27
27
  "sonar-pro-search",
28
28
  "gpt-4o-search",
29
- "grok-3-mini-search",
29
+ "grok-4-fast-search",
30
30
  ]
31
31
  | str
32
32
  )
@@ -47,7 +47,7 @@ Model categories:
47
47
  High-capability models for complex tasks requiring deep reasoning,
48
48
  nuanced understanding, or creative generation.
49
49
 
50
- Small models (gemini-2.5-flash, gpt-5-mini, grok-3-mini):
50
+ Small models (gemini-2.5-flash, gpt-5-mini, grok-4-fast):
51
51
  Efficient models optimized for speed and cost, suitable for
52
52
  simpler tasks or high-volume processing.
53
53
 
@@ -222,6 +222,7 @@ def pipeline_task(
222
222
  trace_input_formatter: Callable[..., str] | None = None,
223
223
  trace_output_formatter: Callable[..., str] | None = None,
224
224
  trace_cost: float | None = None,
225
+ trace_trim_documents: bool = True,
225
226
  # prefect passthrough
226
227
  name: str | None = None,
227
228
  description: str | None = None,
@@ -262,6 +263,7 @@ def pipeline_task(
262
263
  trace_input_formatter: Callable[..., str] | None = None,
263
264
  trace_output_formatter: Callable[..., str] | None = None,
264
265
  trace_cost: float | None = None,
266
+ trace_trim_documents: bool = True,
265
267
  # prefect passthrough
266
268
  name: str | None = None,
267
269
  description: str | None = None,
@@ -318,6 +320,8 @@ def pipeline_task(
318
320
  trace_cost: Optional cost value to track in metadata. When provided and > 0,
319
321
  sets gen_ai.usage.output_cost, gen_ai.usage.cost, and cost metadata.
320
322
  Also forces trace level to "always" if not already set.
323
+ trace_trim_documents: Trim document content in traces to first 100 chars (default True).
324
+ Reduces trace size with large documents.
321
325
 
322
326
  Prefect task parameters:
323
327
  name: Task name (defaults to function name).
@@ -424,6 +428,7 @@ def pipeline_task(
424
428
  ignore_inputs=trace_ignore_inputs,
425
429
  input_formatter=trace_input_formatter,
426
430
  output_formatter=trace_output_formatter,
431
+ trim_documents=trace_trim_documents,
427
432
  )(_wrapper)
428
433
 
429
434
  return cast(
@@ -474,6 +479,7 @@ def pipeline_flow(
474
479
  trace_input_formatter: Callable[..., str] | None = None,
475
480
  trace_output_formatter: Callable[..., str] | None = None,
476
481
  trace_cost: float | None = None,
482
+ trace_trim_documents: bool = True,
477
483
  # prefect passthrough
478
484
  name: str | None = None,
479
485
  version: str | None = None,
@@ -536,6 +542,8 @@ def pipeline_flow(
536
542
  trace_cost: Optional cost value to track in metadata. When provided and > 0,
537
543
  sets gen_ai.usage.output_cost, gen_ai.usage.cost, and cost metadata.
538
544
  Also forces trace level to "always" if not already set.
545
+ trace_trim_documents: Trim document content in traces to first 100 chars (default True).
546
+ Reduces trace size with large documents.
539
547
 
540
548
  Prefect flow parameters:
541
549
  name: Flow name (defaults to function name).
@@ -670,6 +678,7 @@ def pipeline_flow(
670
678
  ignore_inputs=trace_ignore_inputs,
671
679
  input_formatter=trace_input_formatter,
672
680
  output_formatter=trace_output_formatter,
681
+ trim_documents=trace_trim_documents,
673
682
  )(_wrapper)
674
683
 
675
684
  # --- Publish a schema where `documents` accepts str (path) OR DocumentList ---
@@ -18,6 +18,8 @@ Key features:
18
18
  - Jinja2 template rendering with context
19
19
  - Smart path resolution (.jinja2/.jinja extension handling)
20
20
  - Clear error messages for missing templates
21
+ - Built-in global variables:
22
+ - current_date: Current date in format "03 January 2025" (string)
21
23
 
22
24
  Example:
23
25
  >>> from ai_pipeline_core import PromptManager
@@ -45,6 +47,7 @@ Note:
45
47
  The extension can be omitted when calling get().
46
48
  """
47
49
 
50
+ from datetime import datetime
48
51
  from pathlib import Path
49
52
  from typing import Any
50
53
 
@@ -103,6 +106,8 @@ class PromptManager:
103
106
  {% if instructions %}
104
107
  Instructions: {{ instructions }}
105
108
  {% endif %}
109
+
110
+ Date: {{ current_date }} # Current date in format "03 January 2025"
106
111
  ```
107
112
 
108
113
  Note:
@@ -214,6 +219,9 @@ class PromptManager:
214
219
  autoescape=False, # Important for prompt engineering
215
220
  )
216
221
 
222
+ # Add current_date as a global string (format: "03 January 2025")
223
+ self.env.globals["current_date"] = datetime.now().strftime("%d %B %Y") # type: ignore[assignment]
224
+
217
225
  def get(self, prompt_path: str, **kwargs: Any) -> str:
218
226
  """Load and render a Jinja2 template with the given context.
219
227
 
@@ -9,6 +9,7 @@ This module centralizes:
9
9
  from __future__ import annotations
10
10
 
11
11
  import inspect
12
+ import json
12
13
  import os
13
14
  from functools import wraps
14
15
  from typing import Any, Callable, Literal, ParamSpec, TypeVar, cast, overload
@@ -16,6 +17,10 @@ from typing import Any, Callable, Literal, ParamSpec, TypeVar, cast, overload
16
17
  from lmnr import Attributes, Instruments, Laminar, observe
17
18
  from pydantic import BaseModel
18
19
 
20
+ # Import for document trimming - needed for isinstance checks
21
+ # These are lazy imports only used when trim_documents is enabled
22
+ from ai_pipeline_core.documents import Document, DocumentList
23
+ from ai_pipeline_core.llm import AIMessages, ModelResponse
19
24
  from ai_pipeline_core.settings import settings
20
25
 
21
26
  # ---------------------------------------------------------------------------
@@ -34,6 +39,145 @@ Values:
34
39
  """
35
40
 
36
41
 
42
+ # ---------------------------------------------------------------------------
43
+ # Serialization helpers
44
+ # ---------------------------------------------------------------------------
45
+ def _serialize_for_tracing(obj: Any) -> Any:
46
+ """Convert objects to JSON-serializable format for tracing.
47
+
48
+ Handles Pydantic models, Documents, and other special types.
49
+ This is extracted for better testability.
50
+
51
+ Args:
52
+ obj: Object to serialize
53
+
54
+ Returns:
55
+ JSON-serializable representation of the object
56
+ """
57
+ # Our Document types - handle first to ensure serialize_model is used
58
+ if isinstance(obj, Document):
59
+ return obj.serialize_model()
60
+ # DocumentList
61
+ if isinstance(obj, DocumentList):
62
+ return [doc.serialize_model() for doc in obj]
63
+ # AIMessages
64
+ if isinstance(obj, AIMessages):
65
+ result = []
66
+ for msg in obj:
67
+ if isinstance(msg, Document):
68
+ result.append(msg.serialize_model())
69
+ else:
70
+ result.append(msg)
71
+ return result
72
+ # ModelResponse (special Pydantic model) - use standard model_dump
73
+ if isinstance(obj, ModelResponse):
74
+ return obj.model_dump()
75
+ # Pydantic models - use custom serializer that respects Document.serialize_model()
76
+ if isinstance(obj, BaseModel):
77
+ # For Pydantic models, we need to handle Document fields specially
78
+ data = {}
79
+ for field_name, field_value in obj.__dict__.items():
80
+ if isinstance(field_value, Document):
81
+ # Use serialize_model for Documents to get base_type
82
+ data[field_name] = field_value.serialize_model()
83
+ elif isinstance(field_value, BaseModel):
84
+ # Recursively handle nested Pydantic models
85
+ data[field_name] = _serialize_for_tracing(field_value)
86
+ else:
87
+ # Let Pydantic handle other fields normally
88
+ data[field_name] = field_value
89
+ return data
90
+ # Fallback to string representation
91
+ try:
92
+ return str(obj)
93
+ except Exception:
94
+ return f"<{type(obj).__name__}>"
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Document trimming utilities
99
+ # ---------------------------------------------------------------------------
100
+ def _trim_document_content(doc_dict: dict[str, Any]) -> dict[str, Any]:
101
+ """Trim document content based on document type and content type.
102
+
103
+ For non-FlowDocuments:
104
+ - Text content: Keep first 100 and last 100 chars (unless < 250 total)
105
+ - Binary content: Remove content entirely
106
+
107
+ For FlowDocuments:
108
+ - Text content: Keep full content
109
+ - Binary content: Remove content entirely
110
+
111
+ Args:
112
+ doc_dict: Document dictionary with base_type, content, and content_encoding
113
+
114
+ Returns:
115
+ Modified document dictionary with trimmed content
116
+ """
117
+ # Check if this looks like a document (has required fields)
118
+ if not isinstance(doc_dict, dict): # type: ignore[reportUnknownArgumentType]
119
+ return doc_dict
120
+
121
+ if "base_type" not in doc_dict or "content" not in doc_dict:
122
+ return doc_dict
123
+
124
+ base_type = doc_dict.get("base_type")
125
+ content = doc_dict.get("content", "")
126
+ content_encoding = doc_dict.get("content_encoding", "utf-8")
127
+
128
+ # For binary content (base64 encoded), remove content
129
+ if content_encoding == "base64":
130
+ doc_dict = doc_dict.copy()
131
+ doc_dict["content"] = "[binary content removed]"
132
+ return doc_dict
133
+
134
+ # For FlowDocuments with text content, keep full content
135
+ if base_type == "flow":
136
+ return doc_dict
137
+
138
+ # For other documents (task, temporary), trim text content
139
+ if isinstance(content, str) and len(content) > 250:
140
+ doc_dict = doc_dict.copy()
141
+ # Keep first 100 and last 100 characters
142
+ trimmed_chars = len(content) - 200 # Number of characters removed
143
+ doc_dict["content"] = (
144
+ content[:100] + f" ... [trimmed {trimmed_chars} chars] ... " + content[-100:]
145
+ )
146
+
147
+ return doc_dict
148
+
149
+
150
+ def _trim_documents_in_data(data: Any) -> Any:
151
+ """Recursively trim document content in nested data structures.
152
+
153
+ Processes dictionaries, lists, and nested structures to find and trim
154
+ documents based on their type and content.
155
+
156
+ Args:
157
+ data: Input data that may contain documents
158
+
159
+ Returns:
160
+ Data with document content trimmed according to rules
161
+ """
162
+ if isinstance(data, dict):
163
+ # Check if this is a document
164
+ if "base_type" in data and "content" in data:
165
+ # This is a document, trim it
166
+ return _trim_document_content(data)
167
+ else:
168
+ # Recursively process dictionary values
169
+ return {k: _trim_documents_in_data(v) for k, v in data.items()}
170
+ elif isinstance(data, list):
171
+ # Process each item in list
172
+ return [_trim_documents_in_data(item) for item in data]
173
+ elif isinstance(data, tuple):
174
+ # Process tuples
175
+ return tuple(_trim_documents_in_data(item) for item in data)
176
+ else:
177
+ # Return other types unchanged
178
+ return data
179
+
180
+
37
181
  # ---------------------------------------------------------------------------
38
182
  # ``TraceInfo`` – metadata container
39
183
  # ---------------------------------------------------------------------------
@@ -175,6 +319,7 @@ def trace(
175
319
  output_formatter: Callable[..., str] | None = None,
176
320
  ignore_exceptions: bool = False,
177
321
  preserve_global_context: bool = True,
322
+ trim_documents: bool = True,
178
323
  ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
179
324
 
180
325
 
@@ -201,6 +346,7 @@ def trace(
201
346
  output_formatter: Callable[..., str] | None = None,
202
347
  ignore_exceptions: bool = False,
203
348
  preserve_global_context: bool = True,
349
+ trim_documents: bool = True,
204
350
  ) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
205
351
  """Add Laminar observability tracing to any function.
206
352
 
@@ -257,6 +403,12 @@ def trace(
257
403
  preserve_global_context: Maintain Laminar's global context across
258
404
  calls (default True). Set False for isolated traces.
259
405
 
406
+ trim_documents: Automatically trim document content in traces (default True).
407
+ When enabled, non-FlowDocument text content is trimmed to
408
+ first/last 100 chars, and all binary content is removed.
409
+ FlowDocuments keep full text content but binary is removed.
410
+ Helps reduce trace size for large documents.
411
+
260
412
  Returns:
261
413
  Decorated function with same signature but added tracing.
262
414
 
@@ -363,6 +515,72 @@ def trace(
363
515
  _output_formatter = output_formatter
364
516
  _ignore_exceptions = ignore_exceptions
365
517
  _preserve_global_context = preserve_global_context
518
+ _trim_documents = trim_documents
519
+
520
+ # Create document trimming formatters if needed
521
+ def _create_trimming_input_formatter(*args, **kwargs) -> str:
522
+ # First, let any custom formatter process the data
523
+ if _input_formatter:
524
+ result = _input_formatter(*args, **kwargs)
525
+ # If formatter returns string, try to parse and trim
526
+ if isinstance(result, str): # type: ignore[reportUnknownArgumentType]
527
+ try:
528
+ data = json.loads(result)
529
+ trimmed = _trim_documents_in_data(data)
530
+ return json.dumps(trimmed)
531
+ except (json.JSONDecodeError, TypeError):
532
+ return result
533
+ else:
534
+ # If formatter returns dict/list, trim it
535
+ trimmed = _trim_documents_in_data(result)
536
+ return json.dumps(trimmed) if not isinstance(trimmed, str) else trimmed
537
+ else:
538
+ # No custom formatter - mimic Laminar's get_input_from_func_args
539
+ # Build a dict with parameter names as keys (like Laminar does)
540
+ params = list(sig.parameters.keys())
541
+ data = {}
542
+
543
+ # Map args to parameter names
544
+ for i, arg in enumerate(args):
545
+ if i < len(params):
546
+ data[params[i]] = arg
547
+
548
+ # Add kwargs
549
+ data.update(kwargs)
550
+
551
+ # Serialize with our helper function
552
+ serialized = json.dumps(data, default=_serialize_for_tracing)
553
+ parsed = json.loads(serialized)
554
+
555
+ # Trim documents in the serialized data
556
+ trimmed = _trim_documents_in_data(parsed)
557
+ return json.dumps(trimmed)
558
+
559
+ def _create_trimming_output_formatter(result: Any) -> str:
560
+ # First, let any custom formatter process the data
561
+ if _output_formatter:
562
+ formatted = _output_formatter(result)
563
+ # If formatter returns string, try to parse and trim
564
+ if isinstance(formatted, str): # type: ignore[reportUnknownArgumentType]
565
+ try:
566
+ data = json.loads(formatted)
567
+ trimmed = _trim_documents_in_data(data)
568
+ return json.dumps(trimmed)
569
+ except (json.JSONDecodeError, TypeError):
570
+ return formatted
571
+ else:
572
+ # If formatter returns dict/list, trim it
573
+ trimmed = _trim_documents_in_data(formatted)
574
+ return json.dumps(trimmed) if not isinstance(trimmed, str) else trimmed
575
+ else:
576
+ # No custom formatter, serialize result with smart defaults
577
+ # Serialize with our extracted helper function
578
+ serialized = json.dumps(result, default=_serialize_for_tracing)
579
+ parsed = json.loads(serialized)
580
+
581
+ # Trim documents in the serialized data
582
+ trimmed = _trim_documents_in_data(parsed)
583
+ return json.dumps(trimmed)
366
584
 
367
585
  # --- Helper function for runtime logic ---
368
586
  def _prepare_and_get_observe_params(runtime_kwargs: dict[str, Any]) -> dict[str, Any]:
@@ -401,10 +619,19 @@ def trace(
401
619
  observe_params["ignore_output"] = _ignore_output
402
620
  if _ignore_inputs is not None:
403
621
  observe_params["ignore_inputs"] = _ignore_inputs
404
- if _input_formatter is not None:
405
- observe_params["input_formatter"] = _input_formatter
406
- if _output_formatter is not None:
407
- observe_params["output_formatter"] = _output_formatter
622
+
623
+ # Use trimming formatters if trim_documents is enabled
624
+ if _trim_documents:
625
+ # Use the trimming formatters (which may wrap custom formatters)
626
+ observe_params["input_formatter"] = _create_trimming_input_formatter
627
+ observe_params["output_formatter"] = _create_trimming_output_formatter
628
+ else:
629
+ # Use custom formatters directly if provided
630
+ if _input_formatter is not None:
631
+ observe_params["input_formatter"] = _input_formatter
632
+ if _output_formatter is not None:
633
+ observe_params["output_formatter"] = _output_formatter
634
+
408
635
  if _ignore_exceptions:
409
636
  observe_params["ignore_exceptions"] = _ignore_exceptions
410
637
  if _preserve_global_context:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-pipeline-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
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,10 +20,10 @@ 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.6
24
- Requires-Dist: openai>=1.99.9
23
+ Requires-Dist: lmnr>=0.7.13
24
+ Requires-Dist: openai>=1.108.1
25
25
  Requires-Dist: prefect-gcp[cloud-storage]>=0.6.10
26
- Requires-Dist: prefect>=3.4.13
26
+ Requires-Dist: prefect>=3.4.19
27
27
  Requires-Dist: pydantic-settings>=2.10.1
28
28
  Requires-Dist: pydantic>=2.11.7
29
29
  Requires-Dist: python-magic>=0.4.27
@@ -224,9 +224,17 @@ if doc.is_text:
224
224
  # Parse structured data
225
225
  data = doc.as_json() # or as_yaml(), as_pydantic_model()
226
226
 
227
+ # Convert between document types (new in v0.2.1)
228
+ task_doc = flow_doc.model_convert(TaskDocument) # Convert FlowDocument to TaskDocument
229
+ new_doc = doc.model_convert(OtherDocType, content={"new": "data"}) # With content update
230
+
227
231
  # Enhanced filtering (new in v0.1.14)
228
232
  filtered = documents.filter_by([Doc1, Doc2, Doc3]) # Multiple types
229
233
  named = documents.filter_by(["file1.txt", "file2.txt"]) # Multiple names
234
+
235
+ # Immutable collections (new in v0.2.1)
236
+ frozen_docs = DocumentList(docs, frozen=True) # Immutable document list
237
+ frozen_msgs = AIMessages(messages, frozen=True) # Immutable message list
230
238
  ```
231
239
 
232
240
  ### LLM Integration
@@ -312,13 +320,18 @@ async def process_chunk(data: str) -> str:
312
320
  set_trace_cost(0.05) # Track costs (new in v0.1.14)
313
321
  return result
314
322
 
315
- @pipeline_flow(config=MyFlowConfig) # Full observability and orchestration
323
+ @pipeline_flow(
324
+ config=MyFlowConfig,
325
+ trace_trim_documents=True # Trim large documents in traces (new in v0.2.1)
326
+ )
316
327
  async def main_flow(
317
328
  project_name: str,
318
329
  documents: DocumentList,
319
330
  flow_options: FlowOptions
320
331
  ) -> DocumentList:
321
332
  # Your pipeline logic
333
+ # Large documents are automatically trimmed to 100 chars in traces
334
+ # for better observability without overwhelming the tracing UI
322
335
  return DocumentList(results)
323
336
  ```
324
337
 
@@ -1,14 +1,14 @@
1
- ai_pipeline_core/__init__.py,sha256=BBZn5MBlfCWAq1nFwNxsKnvBLfmPB43TnSEH7edde64,5720
1
+ ai_pipeline_core/__init__.py,sha256=LH0lGm02zWS9l7b3uzvvzOfSh7eDPok7RjVTP2_-Mv0,5720
2
2
  ai_pipeline_core/exceptions.py,sha256=vx-XLTw2fJSPs-vwtXVYtqoQUcOc0JeI7UmHqRqQYWU,1569
3
- ai_pipeline_core/pipeline.py,sha256=z3zTHAvDkXAsTJEzkpw1gXonNH8hioNAN2wUybGa1j0,28372
3
+ ai_pipeline_core/pipeline.py,sha256=_00Qctqd7QibyXaetZv6KfyWoW9KZIRdndkYItNHWWI,28921
4
4
  ai_pipeline_core/prefect.py,sha256=91ZgLJHsDsRUW77CpNmkKxYs3RCJuucPM3pjKmNBeDg,2199
5
- ai_pipeline_core/prompt_manager.py,sha256=p7D0vv_nMmny0rmvxrVyYmXPRjmPJo9qI-pRZe4__Bk,11690
5
+ ai_pipeline_core/prompt_manager.py,sha256=FAtb1yK7bGuAeuIJ523LOX9bd7TrcHG-TqZ7Lz4RJC0,12087
6
6
  ai_pipeline_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  ai_pipeline_core/settings.py,sha256=-a9jVGg77xifj2SagCR9shXfzXUd-2MlrlquEu4htG8,5035
8
- ai_pipeline_core/tracing.py,sha256=gy5E4OVr3KX-wZ4zWkk3RSER5Ulw8Q_qyp9YoEMCRj4,21963
8
+ ai_pipeline_core/tracing.py,sha256=9RaJaAX5Vp2C8t73TaY-a9gpVy6a_VtSY0JPohIoQsc,31460
9
9
  ai_pipeline_core/documents/__init__.py,sha256=WHStvGZiSyybOcMTYxSV24U6MA3Am_0_Az5p-DuMFrk,738
10
- ai_pipeline_core/documents/document.py,sha256=dCbKcgemW8nWHEUJdK5MqSe3XTIo-voi6td4hqWqvSw,62270
11
- ai_pipeline_core/documents/document_list.py,sha256=iiW8p5p8PhoUgMJnCnPE5GF5xpf6lWJKIzDyYEf6BkM,13738
10
+ ai_pipeline_core/documents/document.py,sha256=L3S_bfOiViMZLYRcmbV4-s3qO8HoGmqJ5g3bXNVs_3Q,67082
11
+ ai_pipeline_core/documents/document_list.py,sha256=Y_NCjfM_CjkIwHRD2iyGgYBuIykN8lT2IIH_uWOiGis,16254
12
12
  ai_pipeline_core/documents/flow_document.py,sha256=g9wlRJRJgy4RsrrZ_P5Qu6kj0FuUFfhfUsRFgtq4NIM,3918
13
13
  ai_pipeline_core/documents/mime_type.py,sha256=DkW88K95el5nAmhC00XLS0G3WpDXgs5IRsBWbKiqG3Y,7995
14
14
  ai_pipeline_core/documents/task_document.py,sha256=40tFavBLX3FhK9-CRsuOH-3gUZ0zvEkqv9XcMFr8ySk,4077
@@ -17,12 +17,12 @@ ai_pipeline_core/documents/utils.py,sha256=ZyJNjFN7ihWno0K7dJZed7twYmmPLA0z40UzF
17
17
  ai_pipeline_core/flow/__init__.py,sha256=2BfWYMOPYW5teGzwo-qzpn_bom1lxxry0bPsjVgcsCk,188
18
18
  ai_pipeline_core/flow/config.py,sha256=3PCDph2n8dj-txqAvd9Wflbi_6lmfXFR9rUhM-szGSQ,18887
19
19
  ai_pipeline_core/flow/options.py,sha256=2rKR2GifhXcyw8avI_oiEDMLC2jm5Qzpw8z56pbxUMo,2285
20
- ai_pipeline_core/llm/__init__.py,sha256=tSj3Mll8SebivP4J5khdXhM9fnbujnbRh0i5yQRoDJQ,857
21
- ai_pipeline_core/llm/ai_messages.py,sha256=eSmMwTqGvtBeMoWuukzciQRDIIAfs-cnEXjlaADIYkw,9027
22
- ai_pipeline_core/llm/client.py,sha256=pxedLnxb9dEu5I9XHTFgXEYWxMv7HOVHhESxIw1hANA,22946
23
- ai_pipeline_core/llm/model_options.py,sha256=YT_lHazZPa0IbHOuLbWXerRODEDb62sKFM97olSxcAU,7693
24
- ai_pipeline_core/llm/model_response.py,sha256=xKJPsqFHtOGfqpKlsGzyBHPbqjEjNfP-Ix3lGVdiTjQ,15289
25
- ai_pipeline_core/llm/model_types.py,sha256=HrQCe_R86yWv5z_83yB-zoMFp6M5Ee9nSeimZmckqtA,2791
20
+ ai_pipeline_core/llm/__init__.py,sha256=3B_vtEzxrzidP1qOUNQ4RxlUmxZ2MBKQcUhQiTybM9g,661
21
+ ai_pipeline_core/llm/ai_messages.py,sha256=ML4rSCCEEu9_83Mnfn7r4yx0pUkarvnBsrxRZbO4ulw,13126
22
+ ai_pipeline_core/llm/client.py,sha256=oByE8whI1lvyqYUh6q3tKgXJhDiWiJWGztlfoZswrFE,22776
23
+ ai_pipeline_core/llm/model_options.py,sha256=7J9qt7P1qCnSP_NrBzPwx_P-HwkXDYFxKcYzriIJ3U4,7972
24
+ ai_pipeline_core/llm/model_response.py,sha256=iNSKobR3gzZ-CSC8hz8-grgL7jdd2IcnCSX0exdlg7o,15345
25
+ ai_pipeline_core/llm/model_types.py,sha256=2J4Qsb1x21I4eo_VPeaMMOW8shOGPqzJuoGjTLcBFPM,2791
26
26
  ai_pipeline_core/logging/__init__.py,sha256=Nz6-ghAoENsgNmLD2ma9TW9M0U2_QfxuQ5DDW6Vt6M0,651
27
27
  ai_pipeline_core/logging/logging.yml,sha256=YTW48keO_K5bkkb-KXGM7ZuaYKiquLsjsURei8Ql0V4,1353
28
28
  ai_pipeline_core/logging/logging_config.py,sha256=pV2x6GgMPXrzPH27sicCSXfw56beio4C2JKCJ3NsXrg,6207
@@ -32,7 +32,7 @@ ai_pipeline_core/simple_runner/cli.py,sha256=yVyuxLY2RZvdNwmwT5LCe-km2nQJzWTPI0v
32
32
  ai_pipeline_core/simple_runner/simple_runner.py,sha256=f6cIodYkul-Apu1d63T6kR5DZpiaCWpphUcEPp5XjFo,9102
33
33
  ai_pipeline_core/storage/__init__.py,sha256=tcIkjJ3zPBLCyetwiJDewBvS2sbRJrDlBh3gEsQm08E,184
34
34
  ai_pipeline_core/storage/storage.py,sha256=ClMr419Y-eU2RuOjZYd51dC0stWQk28Vb56PvQaoUwc,20007
35
- ai_pipeline_core-0.2.0.dist-info/METADATA,sha256=3U4rWNVFQ_Agwpv6NH2e4k0falKwNpDHn-6GncwbcSs,14556
36
- ai_pipeline_core-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- ai_pipeline_core-0.2.0.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
38
- ai_pipeline_core-0.2.0.dist-info/RECORD,,
35
+ ai_pipeline_core-0.2.2.dist-info/METADATA,sha256=EbqjpaeIwuScRMLTKdfYdut57O8GMUZ-HWYcioQ9r1A,15159
36
+ ai_pipeline_core-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
+ ai_pipeline_core-0.2.2.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
38
+ ai_pipeline_core-0.2.2.dist-info/RECORD,,