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.
- {agentic_blocks-0.1.22/src/agentic_blocks.egg-info → agentic_blocks-0.1.24}/PKG-INFO +5 -1
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/pyproject.toml +5 -1
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/agent.py +60 -3
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/messages.py +18 -8
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24/src/agentic_blocks.egg-info}/PKG-INFO +5 -1
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/SOURCES.txt +0 -4
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/requires.txt +4 -0
- agentic_blocks-0.1.22/src/agentic_blocks/tracing/__init__.py +0 -13
- agentic_blocks-0.1.22/src/agentic_blocks/tracing/config.py +0 -111
- agentic_blocks-0.1.22/src/agentic_blocks/tracing/core.py +0 -287
- agentic_blocks-0.1.22/src/agentic_blocks/tracing/decorator.py +0 -316
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/LICENSE +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/README.md +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/setup.cfg +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/__init__.py +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/llm.py +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/mcp_client.py +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/utils/tools_utils.py +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/visualization/visualize.py +0 -0
- {agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/dependency_links.txt +0 -0
- {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.
|
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.
|
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
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
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.
|
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
|
@@ -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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks/visualization/visualize.py
RENAMED
File without changes
|
{agentic_blocks-0.1.22 → agentic_blocks-0.1.24}/src/agentic_blocks.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|