agentic-blocks 0.1.22__tar.gz → 0.1.24__tar.gz

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 (21) hide show
  1. {agentic_blocks-0.1.22/src/agentic_blocks.egg-info → agentic_blocks-0.1.24}/PKG-INFO +5 -1
  2. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/pyproject.toml +5 -1
  3. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/agent.py +60 -3
  4. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/messages.py +18 -8
  5. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24/src/agentic_blocks.egg-info}/PKG-INFO +5 -1
  6. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/SOURCES.txt +0 -4
  7. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/requires.txt +4 -0
  8. agentic_blocks-0.1.22/src/agentic_blocks/tracing/__init__.py +0 -13
  9. agentic_blocks-0.1.22/src/agentic_blocks/tracing/config.py +0 -111
  10. agentic_blocks-0.1.22/src/agentic_blocks/tracing/core.py +0 -287
  11. agentic_blocks-0.1.22/src/agentic_blocks/tracing/decorator.py +0 -316
  12. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/LICENSE +0 -0
  13. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/README.md +0 -0
  14. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/setup.cfg +0 -0
  15. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/__init__.py +0 -0
  16. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/llm.py +0 -0
  17. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/mcp_client.py +0 -0
  18. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/utils/tools_utils.py +0 -0
  19. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/visualization/visualize.py +0 -0
  20. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/dependency_links.txt +0 -0
  21. {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-blocks
3
- Version: 0.1.22
3
+ Version: 0.1.24
4
4
  Summary: Simple building blocks for agentic AI systems with MCP client and conversation management
5
5
  Author-email: Magnus Bjelkenhed <bjelkenhed@gmail.com>
6
6
  License: MIT
@@ -27,6 +27,10 @@ Requires-Dist: langchain-core
27
27
  Requires-Dist: langfuse
28
28
  Requires-Dist: pocketflow
29
29
  Requires-Dist: pocketflow-tracing
30
+ Requires-Dist: agno
31
+ Requires-Dist: ipywidgets
32
+ Requires-Dist: rich[jupyter]
33
+ Requires-Dist: fastapi
30
34
  Provides-Extra: test
31
35
  Requires-Dist: pytest; extra == "test"
32
36
  Provides-Extra: dev
@@ -14,7 +14,7 @@ agentic_blocks = []
14
14
 
15
15
  [project]
16
16
  name = "agentic-blocks"
17
- version = "0.1.22"
17
+ version = "0.1.24"
18
18
  description = "Simple building blocks for agentic AI systems with MCP client and conversation management"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.11"
@@ -43,6 +43,10 @@ dependencies = [
43
43
  "langfuse",
44
44
  "pocketflow",
45
45
  "pocketflow-tracing",
46
+ "agno",
47
+ "ipywidgets",
48
+ "rich[jupyter]",
49
+ "fastapi",
46
50
  ]
47
51
 
48
52
  [project.urls]
@@ -4,6 +4,18 @@ from agentic_blocks.utils.tools_utils import (
4
4
  execute_pending_tool_calls,
5
5
  )
6
6
  from agentic_blocks import call_llm, Messages
7
+ from rich.panel import Panel
8
+ from rich.box import HEAVY
9
+ from rich.console import Console
10
+ from rich.console import Group
11
+
12
+ console = Console(
13
+ style="black on bright_white",
14
+ force_terminal=True,
15
+ width=None,
16
+ legacy_windows=False,
17
+ color_system="truecolor",
18
+ )
7
19
 
8
20
 
9
21
  class Agent:
@@ -11,6 +23,7 @@ class Agent:
11
23
  self.system_prompt = system_prompt
12
24
  self.tools = tools
13
25
  self.tool_registry = create_tool_registry(tools)
26
+ self.panels = []
14
27
 
15
28
  # Create nodes
16
29
  self.llm_node = self._create_llm_node()
@@ -32,7 +45,8 @@ class Agent:
32
45
  self.tools = tools
33
46
 
34
47
  def prep(self, shared):
35
- return shared["messages"]
48
+ messages = shared["messages"]
49
+ return messages
36
50
 
37
51
  def exec(self, messages) -> Messages:
38
52
  response = call_llm(messages=messages, tools=self.tools)
@@ -49,14 +63,31 @@ class Agent:
49
63
 
50
64
  def _create_tool_node(self):
51
65
  class ToolNode(Node):
52
- def __init__(self, tool_registry):
66
+ def __init__(self, tool_registry, agent):
53
67
  super().__init__()
54
68
  self.tool_registry = tool_registry
69
+ self.agent = agent
55
70
 
56
71
  def prep(self, shared):
57
72
  return shared["messages"]
58
73
 
59
74
  def exec(self, messages) -> Messages:
75
+ tool_calls = messages.get_pending_tool_calls()[0]
76
+ tool_name = tool_calls["tool_name"]
77
+ tool_arguments = tool_calls["arguments"]
78
+
79
+ # Format arguments nicely
80
+ if isinstance(tool_arguments, dict):
81
+ args_str = ", ".join(
82
+ [f"{k}={v}" for k, v in tool_arguments.items()]
83
+ )
84
+ formatted_call = f"{tool_name}({args_str})"
85
+ else:
86
+ formatted_call = f"{tool_name}({tool_arguments})"
87
+
88
+ tool_panel = self.agent.create_panel(formatted_call, "Tool Calls")
89
+ self.agent.panels.append(tool_panel)
90
+
60
91
  tool_responses = execute_pending_tool_calls(
61
92
  messages, self.tool_registry
62
93
  )
@@ -66,7 +97,7 @@ class Agent:
66
97
  def post(self, shared, prep_res, messages):
67
98
  return "llm_node"
68
99
 
69
- return ToolNode(self.tool_registry)
100
+ return ToolNode(self.tool_registry, self)
70
101
 
71
102
  def _create_answer_node(self):
72
103
  class AnswerNode(Node):
@@ -86,3 +117,29 @@ class Agent:
86
117
  self.flow.run(shared)
87
118
 
88
119
  return shared["answer"]
120
+
121
+ def print_response(self, user_prompt: str, stream: bool = False):
122
+ # Reset panels and start with message
123
+ self.panels = []
124
+ message_panel = self.create_panel(user_prompt, "Message")
125
+ self.panels.append(message_panel)
126
+
127
+ # Always collect all panels first
128
+ response = self.invoke(user_prompt)
129
+ response_panel = self.create_panel(response, "Response")
130
+ self.panels.append(response_panel)
131
+
132
+ # Print all panels as a group (no gaps)
133
+ panel_group = Group(*self.panel)
134
+ console.print(panel_group)
135
+
136
+ def create_panel(self, content, title, border_style="blue"):
137
+ return Panel(
138
+ content,
139
+ title=title,
140
+ title_align="left",
141
+ border_style=border_style,
142
+ box=HEAVY,
143
+ expand=True, # Full terminal width
144
+ padding=(1, 1), # Internal padding
145
+ )
@@ -175,6 +175,13 @@ class Messages:
175
175
  """Get the current messages list."""
176
176
  return self.messages
177
177
 
178
+ def get_user_message(self) -> str:
179
+ """Get the user message."""
180
+ for message in reversed(self.messages):
181
+ if message.get("role") == "user":
182
+ return message.get("content") or ""
183
+ return ""
184
+
178
185
  def has_pending_tool_calls(self) -> bool:
179
186
  """
180
187
  Check if the last message has tool calls that need execution.
@@ -213,7 +220,7 @@ class Messages:
213
220
  List of dictionaries with 'tool_name', 'arguments', and 'tool_call_id' keys
214
221
  """
215
222
  pending_calls = []
216
-
223
+
217
224
  if not self.messages:
218
225
  return pending_calls
219
226
 
@@ -234,19 +241,22 @@ class Messages:
234
241
  function_info = tool_call.get("function", {})
235
242
  tool_name = function_info.get("name")
236
243
  arguments_str = function_info.get("arguments", "{}")
237
-
244
+
238
245
  # Parse arguments JSON string to dict
239
246
  import json
247
+
240
248
  try:
241
249
  arguments = json.loads(arguments_str)
242
250
  except json.JSONDecodeError:
243
251
  arguments = {}
244
-
245
- pending_calls.append({
246
- "tool_name": tool_name,
247
- "arguments": arguments,
248
- "tool_call_id": tool_call_id
249
- })
252
+
253
+ pending_calls.append(
254
+ {
255
+ "tool_name": tool_name,
256
+ "arguments": arguments,
257
+ "tool_call_id": tool_call_id,
258
+ }
259
+ )
250
260
 
251
261
  return pending_calls
252
262
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-blocks
3
- Version: 0.1.22
3
+ Version: 0.1.24
4
4
  Summary: Simple building blocks for agentic AI systems with MCP client and conversation management
5
5
  Author-email: Magnus Bjelkenhed <bjelkenhed@gmail.com>
6
6
  License: MIT
@@ -27,6 +27,10 @@ Requires-Dist: langchain-core
27
27
  Requires-Dist: langfuse
28
28
  Requires-Dist: pocketflow
29
29
  Requires-Dist: pocketflow-tracing
30
+ Requires-Dist: agno
31
+ Requires-Dist: ipywidgets
32
+ Requires-Dist: rich[jupyter]
33
+ Requires-Dist: fastapi
30
34
  Provides-Extra: test
31
35
  Requires-Dist: pytest; extra == "test"
32
36
  Provides-Extra: dev
@@ -11,9 +11,5 @@ src/agentic_blocks.egg-info/SOURCES.txt
11
11
  src/agentic_blocks.egg-info/dependency_links.txt
12
12
  src/agentic_blocks.egg-info/requires.txt
13
13
  src/agentic_blocks.egg-info/top_level.txt
14
- src/agentic_blocks/tracing/__init__.py
15
- src/agentic_blocks/tracing/config.py
16
- src/agentic_blocks/tracing/core.py
17
- src/agentic_blocks/tracing/decorator.py
18
14
  src/agentic_blocks/utils/tools_utils.py
19
15
  src/agentic_blocks/visualization/visualize.py
@@ -6,6 +6,10 @@ langchain-core
6
6
  langfuse
7
7
  pocketflow
8
8
  pocketflow-tracing
9
+ agno
10
+ ipywidgets
11
+ rich[jupyter]
12
+ fastapi
9
13
 
10
14
  [dev]
11
15
  pytest
@@ -1,13 +0,0 @@
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"]
@@ -1,111 +0,0 @@
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
@@ -1,287 +0,0 @@
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}")
@@ -1,316 +0,0 @@
1
- """
2
- Decorator for tracing PocketFlow workflows with Langfuse.
3
- """
4
-
5
- import functools
6
- import inspect
7
- import uuid
8
- from typing import Any, Callable, Dict, Optional, Union
9
-
10
- from .config import TracingConfig
11
- from .core import LangfuseTracer
12
-
13
-
14
- def trace_flow(
15
- config: Optional[TracingConfig] = None,
16
- flow_name: Optional[str] = None,
17
- session_id: Optional[str] = None,
18
- user_id: Optional[str] = None,
19
- ):
20
- """
21
- Decorator to add Langfuse tracing to PocketFlow flows.
22
-
23
- This decorator automatically traces:
24
- - Flow execution start/end
25
- - Each node's prep, exec, and post phases
26
- - Input and output data for each phase
27
- - Errors and exceptions
28
-
29
- Args:
30
- config: TracingConfig instance. If None, loads from environment.
31
- flow_name: Custom name for the flow. If None, uses the flow class name.
32
- session_id: Session ID for grouping related traces.
33
- user_id: User ID for the trace.
34
-
35
- Returns:
36
- Decorated flow class or function.
37
-
38
- Example:
39
- ```python
40
- from tracing import trace_flow
41
-
42
- @trace_flow()
43
- class MyFlow(Flow):
44
- def __init__(self):
45
- super().__init__(start=MyNode())
46
-
47
- # Or with custom configuration
48
- config = TracingConfig.from_env()
49
-
50
- @trace_flow(config=config, flow_name="CustomFlow")
51
- class MyFlow(Flow):
52
- pass
53
- ```
54
- """
55
-
56
- def decorator(flow_class_or_func):
57
- # Handle both class and function decoration
58
- if inspect.isclass(flow_class_or_func):
59
- return _trace_flow_class(
60
- flow_class_or_func, config, flow_name, session_id, user_id
61
- )
62
- else:
63
- return _trace_flow_function(
64
- flow_class_or_func, config, flow_name, session_id, user_id
65
- )
66
-
67
- return decorator
68
-
69
-
70
- def _trace_flow_class(flow_class, config, flow_name, session_id, user_id):
71
- """Trace a Flow class by wrapping its methods."""
72
-
73
- # Get or create config
74
- if config is None:
75
- config = TracingConfig.from_env()
76
-
77
- # Override session/user if provided
78
- if session_id:
79
- config.session_id = session_id
80
- if user_id:
81
- config.user_id = user_id
82
-
83
- # Get flow name
84
- if flow_name is None:
85
- flow_name = flow_class.__name__
86
-
87
- # Store original methods
88
- original_init = flow_class.__init__
89
- original_run = getattr(flow_class, "run", None)
90
- original_run_async = getattr(flow_class, "run_async", None)
91
-
92
- def traced_init(self, *args, **kwargs):
93
- """Initialize the flow with tracing capabilities."""
94
- # Call original init
95
- original_init(self, *args, **kwargs)
96
-
97
- # Add tracing attributes
98
- self._tracer = LangfuseTracer(config)
99
- self._flow_name = flow_name
100
- self._trace_id = None
101
-
102
- # Patch all nodes in the flow
103
- self._patch_nodes()
104
-
105
- def traced_run(self, shared):
106
- """Traced version of the run method."""
107
- if not hasattr(self, "_tracer"):
108
- # Fallback if not properly initialized
109
- return original_run(self, shared) if original_run else None
110
-
111
- # Start trace
112
- self._trace_id = self._tracer.start_trace(self._flow_name, shared)
113
-
114
- try:
115
- # Run the original flow
116
- result = original_run(self, shared) if original_run else None
117
-
118
- # End trace successfully
119
- self._tracer.end_trace(shared, "success")
120
-
121
- return result
122
-
123
- except Exception as e:
124
- # End trace with error
125
- self._tracer.end_trace(shared, "error")
126
- raise
127
- finally:
128
- # Ensure cleanup
129
- self._tracer.flush()
130
-
131
- async def traced_run_async(self, shared):
132
- """Traced version of the async run method."""
133
- if not hasattr(self, "_tracer"):
134
- # Fallback if not properly initialized
135
- return (
136
- await original_run_async(self, shared) if original_run_async else None
137
- )
138
-
139
- # Start trace
140
- self._trace_id = self._tracer.start_trace(self._flow_name, shared)
141
-
142
- try:
143
- # Run the original flow
144
- result = (
145
- await original_run_async(self, shared) if original_run_async else None
146
- )
147
-
148
- # End trace successfully
149
- self._tracer.end_trace(shared, "success")
150
-
151
- return result
152
-
153
- except Exception as e:
154
- # End trace with error
155
- self._tracer.end_trace(shared, "error")
156
- raise
157
- finally:
158
- # Ensure cleanup
159
- self._tracer.flush()
160
-
161
- def patch_nodes(self):
162
- """Patch all nodes in the flow to add tracing."""
163
- if not hasattr(self, "start_node") or not self.start_node:
164
- return
165
-
166
- visited = set()
167
- nodes_to_patch = [self.start_node]
168
-
169
- while nodes_to_patch:
170
- node = nodes_to_patch.pop(0)
171
- if id(node) in visited:
172
- continue
173
-
174
- visited.add(id(node))
175
-
176
- # Patch this node
177
- self._patch_node(node)
178
-
179
- # Add successors to patch list
180
- if hasattr(node, "successors"):
181
- for successor in node.successors.values():
182
- if successor and id(successor) not in visited:
183
- nodes_to_patch.append(successor)
184
-
185
- def patch_node(self, node):
186
- """Patch a single node to add tracing."""
187
- if hasattr(node, "_pocketflow_traced"):
188
- return # Already patched
189
-
190
- node_id = str(uuid.uuid4())
191
- node_name = type(node).__name__
192
-
193
- # Store original methods
194
- original_prep = getattr(node, "prep", None)
195
- original_exec = getattr(node, "exec", None)
196
- original_post = getattr(node, "post", None)
197
- original_prep_async = getattr(node, "prep_async", None)
198
- original_exec_async = getattr(node, "exec_async", None)
199
- original_post_async = getattr(node, "post_async", None)
200
-
201
- # Create traced versions
202
- if original_prep:
203
- node.prep = self._create_traced_method(
204
- original_prep, node_id, node_name, "prep"
205
- )
206
- if original_exec:
207
- node.exec = self._create_traced_method(
208
- original_exec, node_id, node_name, "exec"
209
- )
210
- if original_post:
211
- node.post = self._create_traced_method(
212
- original_post, node_id, node_name, "post"
213
- )
214
- if original_prep_async:
215
- node.prep_async = self._create_traced_async_method(
216
- original_prep_async, node_id, node_name, "prep"
217
- )
218
- if original_exec_async:
219
- node.exec_async = self._create_traced_async_method(
220
- original_exec_async, node_id, node_name, "exec"
221
- )
222
- if original_post_async:
223
- node.post_async = self._create_traced_async_method(
224
- original_post_async, node_id, node_name, "post"
225
- )
226
-
227
- # Mark as traced
228
- node._pocketflow_traced = True
229
-
230
- def create_traced_method(self, original_method, node_id, node_name, phase):
231
- """Create a traced version of a synchronous method."""
232
-
233
- @functools.wraps(original_method)
234
- def traced_method(*args, **kwargs):
235
- span_id = self._tracer.start_node_span(node_name, node_id, phase)
236
-
237
- try:
238
- result = original_method(*args, **kwargs)
239
- self._tracer.end_node_span(span_id, input_data=args, output_data=result)
240
- return result
241
- except Exception as e:
242
- self._tracer.end_node_span(span_id, input_data=args, error=e)
243
- raise
244
-
245
- return traced_method
246
-
247
- def create_traced_async_method(self, original_method, node_id, node_name, phase):
248
- """Create a traced version of an asynchronous method."""
249
-
250
- @functools.wraps(original_method)
251
- async def traced_async_method(*args, **kwargs):
252
- span_id = self._tracer.start_node_span(node_name, node_id, phase)
253
-
254
- try:
255
- result = await original_method(*args, **kwargs)
256
- self._tracer.end_node_span(span_id, input_data=args, output_data=result)
257
- return result
258
- except Exception as e:
259
- self._tracer.end_node_span(span_id, input_data=args, error=e)
260
- raise
261
-
262
- return traced_async_method
263
-
264
- # Replace methods on the class
265
- flow_class.__init__ = traced_init
266
- flow_class._patch_nodes = patch_nodes
267
- flow_class._patch_node = patch_node
268
- flow_class._create_traced_method = create_traced_method
269
- flow_class._create_traced_async_method = create_traced_async_method
270
-
271
- if original_run:
272
- flow_class.run = traced_run
273
- if original_run_async:
274
- flow_class.run_async = traced_run_async
275
-
276
- return flow_class
277
-
278
-
279
- def _trace_flow_function(flow_func, config, flow_name, session_id, user_id):
280
- """Trace a flow function (for functional-style flows)."""
281
-
282
- # Get or create config
283
- if config is None:
284
- config = TracingConfig.from_env()
285
-
286
- # Override session/user if provided
287
- if session_id:
288
- config.session_id = session_id
289
- if user_id:
290
- config.user_id = user_id
291
-
292
- # Get flow name
293
- if flow_name is None:
294
- flow_name = flow_func.__name__
295
-
296
- tracer = LangfuseTracer(config)
297
-
298
- @functools.wraps(flow_func)
299
- def traced_flow_func(*args, **kwargs):
300
- # Assume first argument is shared data
301
- shared = args[0] if args else {}
302
-
303
- # Start trace
304
- trace_id = tracer.start_trace(flow_name, shared)
305
-
306
- try:
307
- result = flow_func(*args, **kwargs)
308
- tracer.end_trace(shared, "success")
309
- return result
310
- except Exception as e:
311
- tracer.end_trace(shared, "error")
312
- raise
313
- finally:
314
- tracer.flush()
315
-
316
- return traced_flow_func
File without changes