ai-pipeline-core 0.3.4__py3-none-any.whl → 0.4.0__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.
Files changed (96) hide show
  1. ai_pipeline_core/__init__.py +64 -158
  2. ai_pipeline_core/deployment/__init__.py +6 -18
  3. ai_pipeline_core/deployment/base.py +392 -212
  4. ai_pipeline_core/deployment/contract.py +6 -10
  5. ai_pipeline_core/{utils → deployment}/deploy.py +50 -69
  6. ai_pipeline_core/deployment/helpers.py +16 -17
  7. ai_pipeline_core/{progress.py → deployment/progress.py} +23 -24
  8. ai_pipeline_core/{utils/remote_deployment.py → deployment/remote.py} +11 -14
  9. ai_pipeline_core/docs_generator/__init__.py +54 -0
  10. ai_pipeline_core/docs_generator/__main__.py +5 -0
  11. ai_pipeline_core/docs_generator/cli.py +196 -0
  12. ai_pipeline_core/docs_generator/extractor.py +324 -0
  13. ai_pipeline_core/docs_generator/guide_builder.py +644 -0
  14. ai_pipeline_core/docs_generator/trimmer.py +35 -0
  15. ai_pipeline_core/docs_generator/validator.py +114 -0
  16. ai_pipeline_core/document_store/__init__.py +13 -0
  17. ai_pipeline_core/document_store/_summary.py +9 -0
  18. ai_pipeline_core/document_store/_summary_worker.py +170 -0
  19. ai_pipeline_core/document_store/clickhouse.py +492 -0
  20. ai_pipeline_core/document_store/factory.py +38 -0
  21. ai_pipeline_core/document_store/local.py +312 -0
  22. ai_pipeline_core/document_store/memory.py +85 -0
  23. ai_pipeline_core/document_store/protocol.py +68 -0
  24. ai_pipeline_core/documents/__init__.py +12 -14
  25. ai_pipeline_core/documents/_context_vars.py +85 -0
  26. ai_pipeline_core/documents/_hashing.py +52 -0
  27. ai_pipeline_core/documents/attachment.py +85 -0
  28. ai_pipeline_core/documents/context.py +128 -0
  29. ai_pipeline_core/documents/document.py +318 -1434
  30. ai_pipeline_core/documents/mime_type.py +11 -84
  31. ai_pipeline_core/documents/utils.py +4 -12
  32. ai_pipeline_core/exceptions.py +10 -62
  33. ai_pipeline_core/images/__init__.py +32 -85
  34. ai_pipeline_core/images/_processing.py +5 -11
  35. ai_pipeline_core/llm/__init__.py +6 -4
  36. ai_pipeline_core/llm/ai_messages.py +102 -90
  37. ai_pipeline_core/llm/client.py +229 -183
  38. ai_pipeline_core/llm/model_options.py +12 -84
  39. ai_pipeline_core/llm/model_response.py +53 -99
  40. ai_pipeline_core/llm/model_types.py +8 -23
  41. ai_pipeline_core/logging/__init__.py +2 -7
  42. ai_pipeline_core/logging/logging.yml +1 -1
  43. ai_pipeline_core/logging/logging_config.py +27 -37
  44. ai_pipeline_core/logging/logging_mixin.py +15 -41
  45. ai_pipeline_core/observability/__init__.py +32 -0
  46. ai_pipeline_core/observability/_debug/__init__.py +30 -0
  47. ai_pipeline_core/observability/_debug/_auto_summary.py +94 -0
  48. ai_pipeline_core/{debug/config.py → observability/_debug/_config.py} +11 -7
  49. ai_pipeline_core/{debug/content.py → observability/_debug/_content.py} +133 -75
  50. ai_pipeline_core/{debug/processor.py → observability/_debug/_processor.py} +16 -17
  51. ai_pipeline_core/{debug/summary.py → observability/_debug/_summary.py} +113 -37
  52. ai_pipeline_core/observability/_debug/_types.py +75 -0
  53. ai_pipeline_core/{debug/writer.py → observability/_debug/_writer.py} +126 -196
  54. ai_pipeline_core/observability/_document_tracking.py +146 -0
  55. ai_pipeline_core/observability/_initialization.py +194 -0
  56. ai_pipeline_core/observability/_logging_bridge.py +57 -0
  57. ai_pipeline_core/observability/_summary.py +81 -0
  58. ai_pipeline_core/observability/_tracking/__init__.py +6 -0
  59. ai_pipeline_core/observability/_tracking/_client.py +178 -0
  60. ai_pipeline_core/observability/_tracking/_internal.py +28 -0
  61. ai_pipeline_core/observability/_tracking/_models.py +138 -0
  62. ai_pipeline_core/observability/_tracking/_processor.py +158 -0
  63. ai_pipeline_core/observability/_tracking/_service.py +311 -0
  64. ai_pipeline_core/observability/_tracking/_writer.py +229 -0
  65. ai_pipeline_core/{tracing.py → observability/tracing.py} +139 -335
  66. ai_pipeline_core/pipeline/__init__.py +10 -0
  67. ai_pipeline_core/pipeline/decorators.py +915 -0
  68. ai_pipeline_core/pipeline/options.py +16 -0
  69. ai_pipeline_core/prompt_manager.py +16 -102
  70. ai_pipeline_core/settings.py +26 -31
  71. ai_pipeline_core/testing.py +9 -0
  72. ai_pipeline_core-0.4.0.dist-info/METADATA +807 -0
  73. ai_pipeline_core-0.4.0.dist-info/RECORD +76 -0
  74. ai_pipeline_core/debug/__init__.py +0 -26
  75. ai_pipeline_core/documents/document_list.py +0 -420
  76. ai_pipeline_core/documents/flow_document.py +0 -112
  77. ai_pipeline_core/documents/task_document.py +0 -117
  78. ai_pipeline_core/documents/temporary_document.py +0 -74
  79. ai_pipeline_core/flow/__init__.py +0 -9
  80. ai_pipeline_core/flow/config.py +0 -494
  81. ai_pipeline_core/flow/options.py +0 -75
  82. ai_pipeline_core/pipeline.py +0 -718
  83. ai_pipeline_core/prefect.py +0 -63
  84. ai_pipeline_core/prompt_builder/__init__.py +0 -5
  85. ai_pipeline_core/prompt_builder/documents_prompt.jinja2 +0 -23
  86. ai_pipeline_core/prompt_builder/global_cache.py +0 -78
  87. ai_pipeline_core/prompt_builder/new_core_documents_prompt.jinja2 +0 -6
  88. ai_pipeline_core/prompt_builder/prompt_builder.py +0 -253
  89. ai_pipeline_core/prompt_builder/system_prompt.jinja2 +0 -41
  90. ai_pipeline_core/storage/__init__.py +0 -8
  91. ai_pipeline_core/storage/storage.py +0 -628
  92. ai_pipeline_core/utils/__init__.py +0 -8
  93. ai_pipeline_core-0.3.4.dist-info/METADATA +0 -569
  94. ai_pipeline_core-0.3.4.dist-info/RECORD +0 -57
  95. {ai_pipeline_core-0.3.4.dist-info → ai_pipeline_core-0.4.0.dist-info}/WHEEL +0 -0
  96. {ai_pipeline_core-0.3.4.dist-info → ai_pipeline_core-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,22 @@
1
- """Summary generation for trace debugging.
1
+ """Static summary generation for trace debugging.
2
2
 
3
- Generates a single _summary.md file that serves both human inspection and LLM debugging.
4
- Combines high-level overview with detailed navigation for comprehensive trace analysis.
3
+ Generates _summary.md files with execution tree, LLM calls, cost breakdown,
4
+ and navigation guide. No LLM dependencies pure text formatting.
5
+
6
+ For LLM-powered auto-summary, see _auto_summary.py.
5
7
  """
6
8
 
7
- from .writer import SpanInfo, TraceState
9
+ from typing import Any
10
+
11
+ from ._types import SpanInfo, TraceState
8
12
 
9
13
 
10
- def generate_summary(trace: TraceState) -> str:
14
+ def generate_summary(trace: TraceState) -> str: # noqa: PLR0912, PLR0914, PLR0915
11
15
  """Generate unified _summary.md file.
12
16
 
13
17
  Single file optimized for both human inspection and LLM debugger context.
14
- Structure: Overview Tree Root Span LLM Calls Errors Navigation.
18
+ Structure: Overview -> Tree -> Root Span -> LLM Calls -> Cost by Task -> Errors -> Navigation.
19
+ Cost by Task table includes expected cost comparison with OVER/OK status indicators.
15
20
  """
16
21
  lines = [
17
22
  f"# Trace Summary: {trace.name}",
@@ -20,17 +25,21 @@ def generate_summary(trace: TraceState) -> str:
20
25
 
21
26
  # Status and stats
22
27
  failed_spans = [s for s in trace.spans.values() if s.status == "failed"]
23
- status_emoji = "" if failed_spans else ""
28
+ status_emoji = "\u274c" if failed_spans else "\u2705"
24
29
  status_text = f"Failed ({len(failed_spans)} errors)" if failed_spans else "Completed"
25
30
  duration_str = _format_duration(trace)
26
31
 
32
+ cost_str = f"**Total Cost**: ${trace.total_cost:.4f}"
33
+ if trace.total_expected_cost > 0:
34
+ cost_str += f" (expected: ${trace.total_expected_cost:.4f})"
35
+
27
36
  lines.extend([
28
37
  f"**Status**: {status_emoji} {status_text} | "
29
38
  f"**Duration**: {duration_str} | "
30
39
  f"**Spans**: {len(trace.spans)} | "
31
40
  f"**LLM Calls**: {trace.llm_call_count} | "
32
41
  f"**Total Tokens**: {trace.total_tokens:,} | "
33
- f"**Total Cost**: ${trace.total_cost:.4f}",
42
+ f"{cost_str}",
34
43
  "",
35
44
  ])
36
45
 
@@ -46,8 +55,7 @@ def generate_summary(trace: TraceState) -> str:
46
55
  lines.extend(tree_lines)
47
56
  else:
48
57
  # Fallback: list all spans
49
- for span in sorted(trace.spans.values(), key=lambda s: s.start_time):
50
- lines.append(_format_span_line(span))
58
+ lines.extend(_format_span_line(span) for span in sorted(trace.spans.values(), key=lambda s: s.start_time))
51
59
 
52
60
  lines.extend([
53
61
  "```",
@@ -77,27 +85,48 @@ def generate_summary(trace: TraceState) -> str:
77
85
  lines.extend([
78
86
  "## LLM Calls (by cost)",
79
87
  "",
80
- "| # | Span | Model | Input→Output | Total | Cost | Path |",
81
- "|---|------|-------|--------------|-------|------|------|",
88
+ "| # | Span | Purpose | Model | Input\u2192Output | Total | Cost | Expected | Path |",
89
+ "|---|------|---------|-------|--------------|-------|------|----------|------|",
82
90
  ])
83
91
 
84
92
  for i, span in enumerate(llm_spans, 1):
85
93
  info = span.llm_info
86
94
  if info:
87
95
  model = info.get("model", "unknown")
96
+ purpose = info.get("purpose", "")
88
97
  in_tokens = info.get("input_tokens", 0)
89
98
  out_tokens = info.get("output_tokens", 0)
90
99
  total_tokens = info.get("total_tokens", 0)
91
100
  cost = info.get("cost", 0)
101
+ expected = info.get("expected_cost")
102
+ expected_str = f"${expected:.4f}" if expected else ""
92
103
  span_path = span.path.relative_to(trace.path).as_posix()
93
104
  lines.append(
94
- f"| {i} | {span.name} | {model} | "
95
- f"{in_tokens:,}{out_tokens:,} | {total_tokens:,} | ${cost:.4f} | "
96
- f"`{span_path}/` |"
105
+ f"| {i} | {span.name} | {purpose} | {model} | "
106
+ f"{in_tokens:,}\u2192{out_tokens:,} | {total_tokens:,} | ${cost:.4f} | "
107
+ f"{expected_str} | `{span_path}/` |"
97
108
  )
98
109
 
99
110
  lines.append("")
100
111
 
112
+ # Cost aggregation by parent task/flow
113
+ cost_by_parent = _aggregate_costs_by_parent(trace)
114
+ if cost_by_parent:
115
+ lines.extend([
116
+ "## Cost by Task",
117
+ "",
118
+ "| Name | Type | LLM Calls | Cost | Expected | Status |",
119
+ "|------|------|-----------|------|----------|--------|",
120
+ ])
121
+ for entry in cost_by_parent:
122
+ expected_str = f"${entry['expected_cost']:.4f}" if entry["expected_cost"] else ""
123
+ status = ""
124
+ if entry["expected_cost"] and entry["actual_cost"] > 0:
125
+ ratio = entry["actual_cost"] / entry["expected_cost"]
126
+ status = "OVER" if ratio > 1.1 else "OK"
127
+ lines.append(f"| {entry['name']} | {entry['type']} | {entry['llm_calls']} | ${entry['actual_cost']:.4f} | {expected_str} | {status} |")
128
+ lines.append("")
129
+
101
130
  # Errors
102
131
  if failed_spans:
103
132
  lines.extend([
@@ -122,13 +151,48 @@ def generate_summary(trace: TraceState) -> str:
122
151
  "",
123
152
  "- Each span directory contains `_span.yaml` (metadata), `input.yaml`, `output.yaml`",
124
153
  "- LLM span inputs contain the full message list",
125
- "- `_tree.yaml` has span_id path mapping and full hierarchy",
154
+ "- `_tree.yaml` has span_id \u2192 path mapping and full hierarchy",
126
155
  "",
127
156
  ])
128
157
 
129
158
  return "\n".join(lines)
130
159
 
131
160
 
161
+ def _aggregate_costs_by_parent(trace: TraceState) -> list[dict[str, Any]]:
162
+ """Aggregate LLM costs by parent task/flow span."""
163
+ parent_costs: dict[str, dict[str, Any]] = {}
164
+
165
+ for span in trace.spans.values():
166
+ if not span.llm_info:
167
+ continue
168
+ cost = span.llm_info.get("cost", 0.0)
169
+ if not cost:
170
+ continue
171
+
172
+ # Find parent (task or flow span)
173
+ parent_id = span.parent_id
174
+ if not parent_id or parent_id not in trace.spans:
175
+ continue
176
+ parent = trace.spans[parent_id]
177
+
178
+ if parent_id not in parent_costs:
179
+ run_type = "unknown"
180
+ if parent.prefect_info:
181
+ run_type = parent.prefect_info.get("run_type", "unknown")
182
+ parent_costs[parent_id] = {
183
+ "name": parent.name,
184
+ "type": run_type,
185
+ "actual_cost": 0.0,
186
+ "expected_cost": parent.expected_cost,
187
+ "llm_calls": 0,
188
+ }
189
+ parent_costs[parent_id]["actual_cost"] += cost
190
+ parent_costs[parent_id]["llm_calls"] += 1
191
+
192
+ # Sort by cost descending
193
+ return sorted(parent_costs.values(), key=lambda x: x["actual_cost"], reverse=True)
194
+
195
+
132
196
  def _format_duration(trace: TraceState) -> str:
133
197
  """Format trace duration as human-readable string."""
134
198
  # Calculate from spans if we have them
@@ -147,32 +211,45 @@ def _format_duration(trace: TraceState) -> str:
147
211
 
148
212
  if duration < 1:
149
213
  return f"{int(duration * 1000)}ms"
150
- elif duration < 60:
214
+ if duration < 60:
151
215
  return f"{duration:.1f}s"
152
- elif duration < 3600:
216
+ if duration < 3600:
153
217
  minutes = int(duration // 60)
154
218
  seconds = int(duration % 60)
155
219
  return f"{minutes}m {seconds}s"
156
- else:
157
- hours = int(duration // 3600)
158
- minutes = int((duration % 3600) // 60)
159
- return f"{hours}h {minutes}m"
220
+ hours = int(duration // 3600)
221
+ minutes = int((duration % 3600) // 60)
222
+ return f"{hours}h {minutes}m"
160
223
 
161
224
 
162
225
  def _format_span_line(span: SpanInfo) -> str:
163
226
  """Format a single span as a tree line (without prefix)."""
164
- status_icon = "✅" if span.status == "completed" else "❌" if span.status == "failed" else "⏳"
165
- duration = (
166
- f"{span.duration_ms}ms" if span.duration_ms < 1000 else f"{span.duration_ms / 1000:.1f}s"
167
- )
227
+ if span.status == "completed":
228
+ status_icon = "\u2705"
229
+ elif span.status == "failed":
230
+ status_icon = "\u274c"
231
+ else:
232
+ status_icon = "\u23f3"
233
+ duration = f"{span.duration_ms}ms" if span.duration_ms < 1000 else f"{span.duration_ms / 1000:.1f}s"
234
+
235
+ # Description suffix for task/flow spans
236
+ desc_suffix = ""
237
+ if span.description and span.span_type != "llm":
238
+ desc_suffix = f" -- {span.description}"
168
239
 
240
+ # LLM suffix: show purpose (if available) alongside model, plus cost
169
241
  llm_suffix = ""
170
242
  if span.llm_info:
171
243
  model = span.llm_info.get("model", "?")
172
244
  tokens = span.llm_info.get("total_tokens", 0)
173
- llm_suffix = f" [LLM: {model}, {tokens:,} tokens]"
245
+ cost = span.llm_info.get("cost", 0)
246
+ purpose = span.llm_info.get("purpose")
247
+
248
+ purpose_part = f"{purpose} | " if purpose else ""
249
+ cost_part = f", ${cost:.4f}" if cost else ""
250
+ llm_suffix = f" [LLM: {purpose_part}{model}, {tokens:,} tokens{cost_part}]"
174
251
 
175
- return f"{span.name} ({duration}) {status_icon}{llm_suffix}"
252
+ return f"{span.name} ({duration}) {status_icon}{desc_suffix}{llm_suffix}"
176
253
 
177
254
 
178
255
  def _build_tree(trace: TraceState, span_id: str, prefix: str = "") -> list[str]:
@@ -189,8 +266,8 @@ def _build_tree(trace: TraceState, span_id: str, prefix: str = "") -> list[str]:
189
266
  children = span.children
190
267
  for i, child_id in enumerate(children):
191
268
  is_last = i == len(children) - 1
192
- child_prefix = prefix + ("└── " if is_last else "├── ")
193
- continuation_prefix = prefix + (" " if is_last else " ")
269
+ child_prefix = prefix + ("\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 ")
270
+ continuation_prefix = prefix + (" " if is_last else "\u2502 ")
194
271
 
195
272
  child_span = trace.spans.get(child_id)
196
273
  if child_span:
@@ -200,8 +277,9 @@ def _build_tree(trace: TraceState, span_id: str, prefix: str = "") -> list[str]:
200
277
  # Recursively add all descendants
201
278
  for j, grandchild_id in enumerate(child_span.children):
202
279
  gc_is_last = j == len(child_span.children) - 1
203
- gc_prefix = continuation_prefix + ("└── " if gc_is_last else "├── ")
204
- gc_continuation = continuation_prefix + (" " if gc_is_last else "│ ")
280
+ gc_connector = "\u2514\u2500\u2500 " if gc_is_last else "\u251c\u2500\u2500 "
281
+ gc_prefix = continuation_prefix + gc_connector
282
+ gc_continuation = continuation_prefix + (" " if gc_is_last else "\u2502 ")
205
283
 
206
284
  # Recursively build subtree for grandchild and all its descendants
207
285
  subtree = _build_tree_recursive(trace, grandchild_id, gc_prefix, gc_continuation)
@@ -210,9 +288,7 @@ def _build_tree(trace: TraceState, span_id: str, prefix: str = "") -> list[str]:
210
288
  return lines
211
289
 
212
290
 
213
- def _build_tree_recursive(
214
- trace: TraceState, span_id: str, prefix: str, continuation: str
215
- ) -> list[str]:
291
+ def _build_tree_recursive(trace: TraceState, span_id: str, prefix: str, continuation: str) -> list[str]:
216
292
  """Recursively build tree for a span and all descendants."""
217
293
  lines: list[str] = []
218
294
  span = trace.spans.get(span_id)
@@ -226,8 +302,8 @@ def _build_tree_recursive(
226
302
  children = span.children
227
303
  for i, child_id in enumerate(children):
228
304
  is_last = i == len(children) - 1
229
- child_prefix = continuation + ("└── " if is_last else "├── ")
230
- child_continuation = continuation + (" " if is_last else " ")
305
+ child_prefix = continuation + ("\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 ")
306
+ child_continuation = continuation + (" " if is_last else "\u2502 ")
231
307
 
232
308
  # Recurse for all children
233
309
  subtree = _build_tree_recursive(trace, child_id, child_prefix, child_continuation)
@@ -0,0 +1,75 @@
1
+ """Shared data types for the debug tracing system.
2
+
3
+ Extracted to break the circular dependency between _writer.py and _summary.py:
4
+ _writer needs summary generation functions, _summary needs SpanInfo/TraceState.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class WriteJob:
15
+ """Job for background writer thread."""
16
+
17
+ trace_id: str
18
+ span_id: str
19
+ name: str
20
+ parent_id: str | None
21
+ attributes: dict[str, Any]
22
+ events: list[Any]
23
+ status_code: str # "OK" | "ERROR" | "UNSET"
24
+ status_description: str | None
25
+ start_time_ns: int
26
+ end_time_ns: int
27
+
28
+
29
+ @dataclass
30
+ class SpanInfo:
31
+ """Information about a span for index building.
32
+
33
+ Tracks execution details including timing, LLM metrics (tokens, cost, expected_cost, purpose),
34
+ and Prefect context for observability and cost tracking across the trace hierarchy.
35
+ """
36
+
37
+ span_id: str
38
+ parent_id: str | None
39
+ name: str
40
+ span_type: str
41
+ status: str
42
+ start_time: datetime
43
+ path: Path # Actual directory path for this span
44
+ depth: int = 0 # Nesting depth (0 for root)
45
+ order: int = 0 # Global execution order within trace
46
+ end_time: datetime | None = None
47
+ duration_ms: int = 0
48
+ children: list[str] = field(default_factory=list)
49
+ llm_info: dict[str, Any] | None = None
50
+ prefect_info: dict[str, Any] | None = None
51
+ description: str | None = None
52
+ expected_cost: float | None = None
53
+
54
+
55
+ @dataclass
56
+ class TraceState:
57
+ """State for an active trace.
58
+
59
+ Maintains trace metadata and span hierarchy with accumulated cost
60
+ metrics (total_cost, total_expected_cost) for monitoring resource
61
+ usage and budget tracking during trace execution.
62
+ """
63
+
64
+ trace_id: str
65
+ name: str
66
+ path: Path
67
+ start_time: datetime
68
+ spans: dict[str, SpanInfo] = field(default_factory=dict)
69
+ root_span_id: str | None = None
70
+ total_tokens: int = 0
71
+ total_cost: float = 0.0
72
+ total_expected_cost: float = 0.0
73
+ llm_call_count: int = 0
74
+ span_counter: int = 0 # Global counter for ordering span directories
75
+ merged_wrapper_ids: set[str] = field(default_factory=set) # IDs of merged wrappers