agentic-blocks 0.1.18__py3-none-any.whl → 0.1.20__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.
@@ -0,0 +1,13 @@
1
+ """
2
+ PocketFlow Tracing Module
3
+
4
+ This module provides observability and tracing capabilities for PocketFlow workflows
5
+ using Langfuse as the backend. It includes decorators and utilities to automatically
6
+ trace node execution, inputs, and outputs.
7
+ """
8
+
9
+ from .config import TracingConfig
10
+ from .core import LangfuseTracer
11
+ from .decorator import trace_flow
12
+
13
+ __all__ = ["trace_flow", "TracingConfig", "LangfuseTracer"]
@@ -0,0 +1,111 @@
1
+ """
2
+ Configuration module for PocketFlow tracing with Langfuse.
3
+ """
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+ from dotenv import load_dotenv
9
+
10
+
11
+ @dataclass
12
+ class TracingConfig:
13
+ """Configuration class for PocketFlow tracing with Langfuse."""
14
+
15
+ # Langfuse configuration
16
+ langfuse_secret_key: Optional[str] = None
17
+ langfuse_public_key: Optional[str] = None
18
+ langfuse_host: Optional[str] = None
19
+
20
+ # PocketFlow tracing configuration
21
+ debug: bool = False
22
+ trace_inputs: bool = True
23
+ trace_outputs: bool = True
24
+ trace_prep: bool = True
25
+ trace_exec: bool = True
26
+ trace_post: bool = True
27
+ trace_errors: bool = True
28
+
29
+ # Session configuration
30
+ session_id: Optional[str] = None
31
+ user_id: Optional[str] = None
32
+
33
+ @classmethod
34
+ def from_env(cls, env_file: Optional[str] = None) -> "TracingConfig":
35
+ """
36
+ Create TracingConfig from environment variables.
37
+
38
+ Args:
39
+ env_file: Optional path to .env file. If None, looks for .env in current directory.
40
+
41
+ Returns:
42
+ TracingConfig instance with values from environment variables.
43
+ """
44
+ # Load environment variables from .env file if it exists
45
+ if env_file:
46
+ load_dotenv(env_file)
47
+ else:
48
+ # Try to find .env file in current directory or parent directories
49
+ load_dotenv()
50
+
51
+ return cls(
52
+ langfuse_secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
53
+ langfuse_public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
54
+ langfuse_host=os.getenv("LANGFUSE_HOST"),
55
+ debug=os.getenv("POCKETFLOW_TRACING_DEBUG", "false").lower() == "true",
56
+ trace_inputs=os.getenv("POCKETFLOW_TRACE_INPUTS", "true").lower() == "true",
57
+ trace_outputs=os.getenv("POCKETFLOW_TRACE_OUTPUTS", "true").lower() == "true",
58
+ trace_prep=os.getenv("POCKETFLOW_TRACE_PREP", "true").lower() == "true",
59
+ trace_exec=os.getenv("POCKETFLOW_TRACE_EXEC", "true").lower() == "true",
60
+ trace_post=os.getenv("POCKETFLOW_TRACE_POST", "true").lower() == "true",
61
+ trace_errors=os.getenv("POCKETFLOW_TRACE_ERRORS", "true").lower() == "true",
62
+ session_id=os.getenv("POCKETFLOW_SESSION_ID"),
63
+ user_id=os.getenv("POCKETFLOW_USER_ID"),
64
+ )
65
+
66
+ def validate(self) -> bool:
67
+ """
68
+ Validate that required configuration is present.
69
+
70
+ Returns:
71
+ True if configuration is valid, False otherwise.
72
+ """
73
+ if not self.langfuse_secret_key:
74
+ if self.debug:
75
+ print("Warning: LANGFUSE_SECRET_KEY not set")
76
+ return False
77
+
78
+ if not self.langfuse_public_key:
79
+ if self.debug:
80
+ print("Warning: LANGFUSE_PUBLIC_KEY not set")
81
+ return False
82
+
83
+ if not self.langfuse_host:
84
+ if self.debug:
85
+ print("Warning: LANGFUSE_HOST not set")
86
+ return False
87
+
88
+ return True
89
+
90
+ def to_langfuse_kwargs(self) -> dict:
91
+ """
92
+ Convert configuration to kwargs for Langfuse client initialization.
93
+
94
+ Returns:
95
+ Dictionary of kwargs for Langfuse client.
96
+ """
97
+ kwargs = {}
98
+
99
+ if self.langfuse_secret_key:
100
+ kwargs["secret_key"] = self.langfuse_secret_key
101
+
102
+ if self.langfuse_public_key:
103
+ kwargs["public_key"] = self.langfuse_public_key
104
+
105
+ if self.langfuse_host:
106
+ kwargs["host"] = self.langfuse_host
107
+
108
+ if self.debug:
109
+ kwargs["debug"] = True
110
+
111
+ return kwargs
@@ -0,0 +1,287 @@
1
+ """
2
+ Core tracing functionality for PocketFlow with Langfuse integration.
3
+ """
4
+
5
+ import json
6
+ import time
7
+ import uuid
8
+ from typing import Any, Dict, Optional, Union
9
+ from datetime import datetime
10
+
11
+ try:
12
+ from langfuse import Langfuse
13
+
14
+ LANGFUSE_AVAILABLE = True
15
+ except ImportError:
16
+ LANGFUSE_AVAILABLE = False
17
+ print("Warning: langfuse package not installed. Install with: pip install langfuse")
18
+
19
+ from .config import TracingConfig
20
+
21
+
22
+ class LangfuseTracer:
23
+ """
24
+ Core tracer class that handles Langfuse integration for PocketFlow.
25
+ """
26
+
27
+ def __init__(self, config: TracingConfig):
28
+ """
29
+ Initialize the LangfuseTracer.
30
+
31
+ Args:
32
+ config: TracingConfig instance with Langfuse settings.
33
+ """
34
+ self.config = config
35
+ self.client = None
36
+ self.current_trace = None
37
+ self.spans = {} # Store spans by node ID
38
+
39
+ if LANGFUSE_AVAILABLE and config.validate():
40
+ try:
41
+ # Initialize Langfuse client with proper parameters
42
+ kwargs = {}
43
+ if config.langfuse_secret_key:
44
+ kwargs["secret_key"] = config.langfuse_secret_key
45
+ if config.langfuse_public_key:
46
+ kwargs["public_key"] = config.langfuse_public_key
47
+ if config.langfuse_host:
48
+ kwargs["host"] = config.langfuse_host
49
+ if config.debug:
50
+ kwargs["debug"] = True
51
+
52
+ self.client = Langfuse(**kwargs)
53
+ if config.debug:
54
+ print(
55
+ f"✓ Langfuse client initialized with host: {config.langfuse_host}"
56
+ )
57
+ except Exception as e:
58
+ if config.debug:
59
+ print(f"✗ Failed to initialize Langfuse client: {e}")
60
+ self.client = None
61
+ else:
62
+ if config.debug:
63
+ print("✗ Langfuse not available or configuration invalid")
64
+
65
+ def start_trace(self, flow_name: str, input_data: Dict[str, Any]) -> Optional[str]:
66
+ """
67
+ Start a new trace for a flow execution.
68
+
69
+ Args:
70
+ flow_name: Name of the flow being traced.
71
+ input_data: Input data for the flow.
72
+
73
+ Returns:
74
+ Trace ID if successful, None otherwise.
75
+ """
76
+ if not self.client:
77
+ return None
78
+
79
+ try:
80
+ # Serialize input data safely
81
+ serialized_input = self._serialize_data(input_data)
82
+
83
+ # Use Langfuse v2 API to create a trace
84
+ self.current_trace = self.client.trace(
85
+ name=flow_name,
86
+ input=serialized_input,
87
+ metadata={
88
+ "framework": "PocketFlow",
89
+ "trace_type": "flow_execution",
90
+ "timestamp": datetime.now().isoformat(),
91
+ },
92
+ session_id=self.config.session_id,
93
+ user_id=self.config.user_id,
94
+ )
95
+
96
+ # Get the trace ID
97
+ trace_id = self.current_trace.id
98
+
99
+ if self.config.debug:
100
+ print(f"✓ Started trace: {trace_id} for flow: {flow_name}")
101
+
102
+ return trace_id
103
+
104
+ except Exception as e:
105
+ if self.config.debug:
106
+ print(f"✗ Failed to start trace: {e}")
107
+ return None
108
+
109
+ def end_trace(self, output_data: Dict[str, Any], status: str = "success") -> None:
110
+ """
111
+ End the current trace.
112
+
113
+ Args:
114
+ output_data: Output data from the flow.
115
+ status: Status of the trace execution.
116
+ """
117
+ if not self.current_trace:
118
+ return
119
+
120
+ try:
121
+ # Serialize output data safely
122
+ serialized_output = self._serialize_data(output_data)
123
+
124
+ # Update the trace with output data using v2 API
125
+ self.current_trace.update(
126
+ output=serialized_output,
127
+ metadata={
128
+ "status": status,
129
+ "end_timestamp": datetime.now().isoformat(),
130
+ },
131
+ )
132
+
133
+ if self.config.debug:
134
+ print(f"✓ Ended trace with status: {status}")
135
+
136
+ except Exception as e:
137
+ if self.config.debug:
138
+ print(f"✗ Failed to end trace: {e}")
139
+ finally:
140
+ self.current_trace = None
141
+ self.spans.clear()
142
+
143
+ def start_node_span(
144
+ self, node_name: str, node_id: str, phase: str
145
+ ) -> Optional[str]:
146
+ """
147
+ Start a span for a node execution phase.
148
+
149
+ Args:
150
+ node_name: Name/type of the node.
151
+ node_id: Unique identifier for the node instance.
152
+ phase: Execution phase (prep, exec, post).
153
+
154
+ Returns:
155
+ Span ID if successful, None otherwise.
156
+ """
157
+ if not self.current_trace:
158
+ return None
159
+
160
+ try:
161
+ span_id = f"{node_id}_{phase}"
162
+
163
+ # Create a child span using v2 API
164
+ span = self.current_trace.span(
165
+ name=f"{node_name}.{phase}",
166
+ metadata={
167
+ "node_type": node_name,
168
+ "node_id": node_id,
169
+ "phase": phase,
170
+ "start_timestamp": datetime.now().isoformat(),
171
+ },
172
+ )
173
+
174
+ self.spans[span_id] = span
175
+
176
+ if self.config.debug:
177
+ print(f"✓ Started span: {span_id}")
178
+
179
+ return span_id
180
+
181
+ except Exception as e:
182
+ if self.config.debug:
183
+ print(f"✗ Failed to start span: {e}")
184
+ return None
185
+
186
+ def end_node_span(
187
+ self,
188
+ span_id: str,
189
+ input_data: Any = None,
190
+ output_data: Any = None,
191
+ error: Exception = None,
192
+ ) -> None:
193
+ """
194
+ End a node execution span.
195
+
196
+ Args:
197
+ span_id: ID of the span to end.
198
+ input_data: Input data for the phase.
199
+ output_data: Output data from the phase.
200
+ error: Exception if the phase failed.
201
+ """
202
+ if span_id not in self.spans:
203
+ return
204
+
205
+ try:
206
+ span = self.spans[span_id]
207
+
208
+ # Prepare update data
209
+ update_data = {}
210
+
211
+ if input_data is not None and self.config.trace_inputs:
212
+ update_data["input"] = self._serialize_data(input_data)
213
+ if output_data is not None and self.config.trace_outputs:
214
+ update_data["output"] = self._serialize_data(output_data)
215
+
216
+ if error and self.config.trace_errors:
217
+ update_data.update(
218
+ {
219
+ "level": "ERROR",
220
+ "status_message": str(error),
221
+ "metadata": {
222
+ "error_type": type(error).__name__,
223
+ "error_message": str(error),
224
+ "end_timestamp": datetime.now().isoformat(),
225
+ },
226
+ }
227
+ )
228
+ else:
229
+ update_data.update(
230
+ {
231
+ "level": "DEFAULT",
232
+ "metadata": {"end_timestamp": datetime.now().isoformat()},
233
+ }
234
+ )
235
+
236
+ # Update the span with all data at once
237
+ span.update(**update_data)
238
+
239
+ # End the span
240
+ span.end()
241
+
242
+ if self.config.debug:
243
+ status = "ERROR" if error else "SUCCESS"
244
+ print(f"✓ Ended span: {span_id} with status: {status}")
245
+
246
+ except Exception as e:
247
+ if self.config.debug:
248
+ print(f"✗ Failed to end span: {e}")
249
+ finally:
250
+ if span_id in self.spans:
251
+ del self.spans[span_id]
252
+
253
+ def _serialize_data(self, data: Any) -> Any:
254
+ """
255
+ Safely serialize data for Langfuse.
256
+
257
+ Args:
258
+ data: Data to serialize.
259
+
260
+ Returns:
261
+ Serialized data that can be sent to Langfuse.
262
+ """
263
+ try:
264
+ # Handle common PocketFlow data types
265
+ if hasattr(data, "__dict__"):
266
+ # Convert objects to dict representation
267
+ return {"_type": type(data).__name__, "_data": str(data)}
268
+ elif isinstance(data, (dict, list, str, int, float, bool, type(None))):
269
+ # JSON-serializable types
270
+ return data
271
+ else:
272
+ # Fallback to string representation
273
+ return {"_type": type(data).__name__, "_data": str(data)}
274
+ except Exception:
275
+ # Ultimate fallback
276
+ return {"_type": "unknown", "_data": "<serialization_failed>"}
277
+
278
+ def flush(self) -> None:
279
+ """Flush any pending traces to Langfuse."""
280
+ if self.client:
281
+ try:
282
+ self.client.flush()
283
+ if self.config.debug:
284
+ print("✓ Flushed traces to Langfuse")
285
+ except Exception as e:
286
+ if self.config.debug:
287
+ print(f"✗ Failed to flush traces: {e}")