SwisperStudio SDK Integration Guide¶
Version: v1.4 Last Updated: 2025-12-04 Last Updated By: heiko Status: Active
Changelog¶
v1.4 - 2025-12-04¶
- Added
@traceddecorator documentation - NEW section for tracing individual functions - Added LLM observation type detection explained - How AUTO detection works
- Clarified
wrap_llm_adapter()is CRITICAL for LLM node detection - Added evidence from investigation - SDK must call wrapped LLM methods inside
@tracedscope - Added decorator examples - Basic, with explicit type, nested observations
v1.3 - 2025-12-04¶
- Updated to SDK v0.5.2 (latest stable version)
- Added reference to LLM Observation Type Investigation document
- Added troubleshooting for LLM nodes showing as PROC instead of LLM
- Added explicit
observation_type="GENERATION"workaround for LLM nodes - Cross-referenced with
docs/investigations/LLM_OBSERVATION_TYPE_INVESTIGATION.md
v1.2 - 2025-11-18¶
- BREAKING CHANGE: Switched SDK distribution from GitHub Packages to public PyPI
- Simplified installation from 10 minutes to 2 minutes
- Removed all GitHub Packages authentication steps (deprecated)
- Updated to simple
pip install swisper-studio-sdk==0.5.2 - Added Quick Start section at top for immediate installation
- Cleaned up Docker setup (no GitHub token needed)
- Removed deprecated SDK path manipulation from initialization code
- Updated troubleshooting for PyPI installation
- Reduced total integration time from 2 hours to 1 hour 45 min
v1.1 - 2025-11-08¶
- Added Step-by-Step Integration Tutorial section
- Added prerequisites checklist
- Added 8-step implementation guide with time estimates
- Added verification steps for each phase
- Added completion checklist (40+ items)
- Added troubleshooting guide (5 common issues)
- Added estimated timeline (2 hours total)
- Added next steps after integration
v1.0 - 2025-11-08¶
- Initial documentation of SwisperStudio SDK v0.5.2 integration
- Documented state changes across all agents
- Documented graph wrapper pattern
- Documented tool execution standardization
- Documented configuration and initialization
Overview¶
This guide documents the integration of SwisperStudio SDK v0.5.2 into the Swisper backend for observability and tracing. The integration enables:
- ✅ LangGraph execution tracing - Track state transitions across all agents
- ✅ Tool execution monitoring - Capture all tool calls with parameters and results
- ✅ LLM prompt/response capture - Record all LLM interactions (including streaming)
- ✅ Redis Streams-based transport - 50x faster than HTTP polling
- ✅ Minimal code changes - Clean decorator pattern with graceful fallback
🚀 Quick Start - SDK Installation¶
Before you begin integration, install the SDK:
# Install from PyPI (public package)
pip install swisper-studio-sdk==0.5.2
# Verify it works
python -c "from swisper_studio_sdk import create_traced_graph; print('✅ Ready!')"
PyPI Package: https://pypi.org/project/swisper-studio-sdk/
That's it! Now proceed with the integration steps below.
Table of Contents¶
- Step-by-Step Integration Tutorial ⭐ START HERE
- The
@tracedDecorator ⭐ NEW - LLM Observation Type Detection ⭐ NEW
- Integration Summary
- State Changes
- Graph Wrapper Pattern
- Tool Execution Standardization
- Configuration
- Initialization
- Testing
- Backward Compatibility
Step-by-Step Integration Tutorial¶
Target Audience: Developers integrating SwisperStudio SDK into a new LangGraph-based application
Estimated Time: 90-120 minutes
What You'll Achieve: Full observability with LangGraph tracing, tool execution monitoring, and LLM prompt capture
Prerequisites¶
Before starting, ensure you have:
- ✅ Python 3.11+ installed
- ✅ Python backend with LangGraph agents (StateGraph-based)
- ✅ Redis running (for event streaming)
- ✅ Git branch for integration work
- ✅ SwisperStudio deployed (see deployment section below)
Verify Prerequisites:
# Check Python version (3.11+)
python --version
# Check pip is installed
pip --version
# Check Redis is running
redis-cli ping
# Should return: PONG
SwisperStudio Deployment & URLs¶
SwisperStudio must be deployed before integration.
Development Environment¶
URLs:
- Backend API: http://localhost:8001
- Frontend UI: http://localhost:3000
- Redis: redis://redis:6379 (shared with Swisper)
Staging Environment (Update with your actual URLs)¶
- Backend API:
https://swisper-studio-api-staging.fintama.com← Configure this - Frontend UI:
https://swisper-studio-staging.fintama.com← Configure this - Redis:
redis://swisper-redis-staging.internal:6379← Configure this
Production Environment (Update with your actual URLs)¶
- Backend API:
https://swisper-studio-api.fintama.com← Configure this - Frontend UI:
https://swisper-studio.fintama.com← Configure this - Redis:
redis://swisper-redis.internal:6379← Configure this
Note: Replace placeholder URLs with your actual infrastructure endpoints.
Step 1: Install SwisperStudio SDK (2 min)¶
Objective: Install SDK from PyPI (Public Package Repository)
📦 TLDR - Quick Install:
# One command - no authentication needed! pip install swisper-studio-sdk==0.5.2 # Verify python -c "from swisper_studio_sdk import create_traced_graph; print('✅ Works!')"That's it! The SDK is on public PyPI. 🎉
Step-by-Step Installation:¶
Step 1.1: Install SDK from PyPI¶
Single Command Installation:
No authentication required! The package is published on PyPI.
Step 1.2: Verify Installation¶
# Test import
python -c "from swisper_studio_sdk import create_traced_graph, initialize_redis_publisher; print('✅ SDK installed successfully!')"
# Check installed version
pip show swisper-studio-sdk
# Expected output:
# Name: swisper-studio-sdk
# Version: 0.5.2
# Summary: SwisperStudio SDK - Tracing integration for Swisper (internal use)
# Location: /path/to/site-packages
Add to requirements.txt:¶
For your project's requirements.txt:
Then install all dependencies:
Docker Setup (Production/Staging):¶
Update Dockerfile:
# Install dependencies from requirements.txt
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
Add to requirements.txt:
Build and run:
That's it! No special authentication needed since PyPI is public.
Verification:¶
After installation (local or Docker), verify the SDK is working:
# Test import
python -c "from swisper_studio_sdk import create_traced_graph, initialize_redis_publisher; print('✅ SDK installed successfully!')"
# Check version
python -c "import swisper_studio_sdk; print(f'SDK version: 0.5.2')"
Expected output:
Common Installation Issues:¶
| Issue | Cause | Solution |
|---|---|---|
Could not find a version |
Package not on PyPI yet | Wait for publish workflow to complete or check https://pypi.org/project/swisper-studio-sdk/ |
ImportError after install |
Wrong Python environment | Activate correct venv: source venv/bin/activate |
No module named 'swisper_studio_sdk' |
Package name typo | Use exact name: swisper-studio-sdk (with hyphen) |
Version conflict |
Incompatible dependencies | Check requirements: Python 3.11+, LangGraph 1.x |
Network error |
PyPI unreachable | Check internet connection or use mirror |
Step 2: Add Configuration (5 min)¶
Objective: Configure SwisperStudio settings in your app config
File: apps/backend/swisper/core/config.py
Add these fields to your Settings class:
# SwisperStudio Integration (Observability & Configuration Management)
SWISPER_STUDIO_ENABLED: bool = True
SWISPER_STUDIO_REDIS_URL: str = "redis://redis:6379" # Redis for observability events
SWISPER_STUDIO_STREAM_NAME: str = "observability:events"
SWISPER_STUDIO_PROJECT_ID: str = "0d7aa606-cb29-4a31-8a59-50fa61151a32" # Your project ID
SWISPER_STUDIO_CAPTURE_REASONING: bool = True # Capture LLM reasoning (<think> tags)
SWISPER_STUDIO_REASONING_MAX_LENGTH: int = 50000 # 50 KB limit
Verify:
cd backend
python -c "from app.core.config import settings; print(f'Enabled: {settings.SWISPER_STUDIO_ENABLED}')"
# Should print: Enabled: True
How to Get Project ID:
- Open SwisperStudio UI (development:
http://localhost:3000, production:https://swisper-studio.fintama.com) - Navigate to "Projects"
- Create a new project (e.g., "Swisper Production" or "Swisper Development")
- Click on the project
- Copy the UUID from the URL:
/projects/{THIS_IS_YOUR_PROJECT_ID}/... - Paste into
SWISPER_STUDIO_PROJECT_IDfield
Notes:
- Create separate projects for dev/staging/production environments
- Set SWISPER_STUDIO_ENABLED: bool = False to disable observability
Step 3: Add Initialization & Shutdown (10 min)¶
Objective: Initialize Redis publisher on startup, close on shutdown
File: apps/backend/app/main.py
Add to your startup function (lifespan or @app.on_event("startup")):
# Initialize SwisperStudio observability (SDK v0.5.2)
if settings.SWISPER_STUDIO_ENABLED:
try:
from swisper_studio_sdk import initialize_redis_publisher, wrap_llm_adapter
# Initialize Redis Streams publisher for observability
await initialize_redis_publisher(
redis_url=settings.SWISPER_STUDIO_REDIS_URL,
stream_name=settings.SWISPER_STUDIO_STREAM_NAME,
project_id=settings.SWISPER_STUDIO_PROJECT_ID,
verify_consumer=True, # Check if SwisperStudio consumer is running
)
logger.info("✅ SwisperStudio observability initialized (Redis Streams)")
logger.info(f" Redis: {settings.SWISPER_STUDIO_REDIS_URL}")
logger.info(f" Stream: {settings.SWISPER_STUDIO_STREAM_NAME}")
logger.info(f" Project ID: {settings.SWISPER_STUDIO_PROJECT_ID}")
# ⚠️ CRITICAL: Enable LLM prompt capture for AUTO type detection
# Without this, ALL observations will be SPAN (not GENERATION)!
try:
wrap_llm_adapter()
logger.info("✅ LLM prompt capture enabled (structured + streaming)")
logger.info(" AUTO type detection will work for @traced decorators")
except Exception as e:
logger.warning(f"⚠️ LLM prompt capture disabled: {e}")
logger.warning(" LLM nodes will show as SPAN instead of GENERATION!")
logger.warning(" Workaround: Use @traced(observation_type='GENERATION')")
except ImportError:
logger.warning("⚠️ SwisperStudio SDK not installed - observability disabled")
logger.warning(" Install with: pip install swisper-studio-sdk==0.5.2")
except Exception as e:
logger.warning(f"⚠️ SwisperStudio observability initialization failed: {e}")
logger.warning(" Continuing without observability")
⚠️ CRITICAL: The
wrap_llm_adapter()call is essential for LLM observation type detection! If you skip this, ALL your LLM-calling functions will appear as SPAN nodes, not GENERATION.
Add to your shutdown function:
# Close SwisperStudio Redis publisher
if settings.SWISPER_STUDIO_ENABLED:
try:
from swisper_studio_sdk import close_redis_publisher
await close_redis_publisher()
logger.info("✅ SwisperStudio Redis publisher closed")
except Exception as e:
logger.warning(f"⚠️ Failed to close SwisperStudio publisher: {e}")
Verify:
# Start your backend
fastapi run app/main.py
# Check logs - should see:
# ✅ SwisperStudio observability initialized (Redis Streams)
# ✅ LLM prompt capture enabled (structured + streaming)
Troubleshooting:
- ❌ Redis connection failed: Check
SWISPER_STUDIO_REDIS_URLis correct, Redis is running - ❌ verify_consumer failed: SwisperStudio consumer not running (OK for now, just shows warning)
Step 4: Update Agent State Schemas (15 min)¶
Objective: Add standardized _tools_executed fields to all agent states
Pattern to Apply:
For each agent state TypedDict, add:
_tools_executed: List[Dict[str, Any]] # STANDARD FORMAT: Tool execution trace
_tools_executed_by: Optional[str] # OWNERSHIP MARKER: Which node created this
Files to Update:
- Global Supervisor State
- File:
apps/backend/swisper/models/state/global_supervisor.py -
Type:
Optional[List[Dict[str, Any]]](aggregates from all agents) -
Research Agent State
- File:
apps/backend/swisper/models/state/research.py -
Type:
List[Dict[str, Any]] -
Productivity Agent State
- File:
apps/backend/swisper/models/state/productivity.py -
Type:
List[Dict[str, Any]] -
Wealth Agent State
- File:
apps/backend/swisper/models/state/wealth.py -
Type:
List[Dict[str, Any]] -
Document Agent State
- File:
apps/backend/swisper/models/state/doc.py - Type:
List[Dict[str, Any]]
Example (Research Agent):
class ResearchAgentState(TypedDict):
# ... existing fields ...
tool_execution_results_history: List[ToolExecutionResult] # Backwards compat
# NEW: SwisperStudio observability
_tools_executed: List[Dict[str, Any]]
_tools_executed_by: Optional[str]
iteration_count: int
Verify:
# Run type checker
cd backend
mypy app/api/services/agents/
# Should pass without errors (or same errors as before)
Checklist:
- [ ] Global Supervisor State updated
- [ ] Research Agent State updated
- [ ] Productivity Agent State updated
- [ ] Wealth Agent State updated
- [ ] Document Agent State updated
Step 5: Wrap Agent Graphs (20 min)¶
Objective: Wrap all StateGraph instances with create_traced_graph() for tracing
Pattern:
Replace direct StateGraph(AgentState) with try/except wrapper:
# ❌ OLD: Direct StateGraph
workflow = StateGraph(AgentState)
# ✅ NEW: Traced graph with fallback
try:
from swisper_studio_sdk import create_traced_graph
workflow = create_traced_graph(
AgentState,
trace_name="agent_name" # Use descriptive name
)
self.logger.info("✅ Agent graph wrapped with SwisperStudio tracing")
except ImportError:
workflow = StateGraph(AgentState)
self.logger.warning("⚠️ SwisperStudio SDK not available - tracing disabled")
Files to Update:
- Global Supervisor
- File:
apps/backend/swisper/agents/supervisor/agent.py - Location: In
build_graph()method (around line 175) -
Trace name:
"global_supervisor" -
Research Agent
- File:
apps/backend/swisper/agents/research/agent.py - Location: In
build_graph()method (around line 269) -
Trace name:
"research_agent" -
Productivity Agent
- File:
apps/backend/swisper/agents/productivity/agent.py - Location: In
build_graph()method (around line 141) -
Trace name:
"productivity_agent" -
Wealth Agent
- File:
apps/backend/swisper/agents/wealth/agent.py - Location: In
build_graph()method (around line 236) -
Trace name:
"wealth_agent" -
Document Agent
- File:
apps/backend/swisper/agents/doc/agent.py - Location: In
build_graph()method (around line 108) - Trace name:
"document_search_agent"
Verify:
# Start backend
fastapi run app/main.py
# Send a test message that triggers agents
# Check logs for:
# ✅ GlobalSupervisor graph wrapped with SwisperStudio tracing
# ✅ ResearchAgent graph wrapped with SwisperStudio tracing
# ✅ ProductivityAgent graph wrapped with SwisperStudio tracing
# ✅ WealthAgent graph wrapped with SwisperStudio tracing
# ✅ DocumentSearchAgent graph wrapped with SwisperStudio tracing
Checklist:
- [ ] Global Supervisor graph wrapped
- [ ] Research Agent graph wrapped
- [ ] Productivity Agent graph wrapped
- [ ] Wealth Agent graph wrapped
- [ ] Document Agent graph wrapped
Step 6: Update Tool Execution Nodes (30 min)¶
Objective: Populate _tools_executed in standardized format in all tool nodes
⚠️ CRITICAL: Ownership Marker MUST Match Node Name¶
The _tools_executed_by value MUST EXACTLY match the node name used in add_node().
Why: The SDK decorator checks: if node_name == ownership_marker before extracting tools.
Example - Correct Setup:
# In agent build_graph() method:
workflow.add_node("tool_execution", self.tool_execution_node.execute) # Node name
# In tool_execution_node.py:
state["_tools_executed_by"] = "tool_execution" # ✅ MUST MATCH!
Example - Wrong (Will Break):
# In agent build_graph():
workflow.add_node("tool_execution", self.tool_execution_node.execute)
# In tool_execution_node.py:
state["_tools_executed_by"] = "wealth_tool_execution" # ❌ MISMATCH! Tools won't appear!
Result of Mismatch:
- ❌ No wrench icons in UI
- ❌ Tool parameters not visible
- ❌ Tools are skipped in extraction
- ❌ Logs show: ⏭️ Skipping _tools_executed
Standard Pattern:¶
# STEP 1: Execute tool (your existing logic)
result = await execute_tool(params)
# STEP 2: Create standardized tool entry
tool_entry = {
"tool_name": "search_web", # Tool/function name
"parameters": {"query": "..."}, # Input parameters
"result": result, # Execution result
"timestamp": datetime.now(timezone.utc).isoformat(), # ISO 8601
"status": "success" if success else "failure"
}
# STEP 3: Append to _tools_executed
existing_tools_executed = state.get("_tools_executed", [])
updated_tools_executed = existing_tools_executed + [tool_entry]
# STEP 4: Update state with ownership marker
# ⚠️ CRITICAL: Use the EXACT node name from add_node()
state["_tools_executed"] = updated_tools_executed
state["_tools_executed_by"] = "tool_execution" # MUST match add_node("tool_execution", ...)
Files to Update:
- Research Agent - Tool Execution Node
- File:
apps/backend/swisper/agents/research/nodes/tool_execution_node.py - Function:
execute()method - Node name:
"tool_execution"(checkresearch_agent.py) -
Ownership:
"tool_execution"⚠️ MUST match node name! -
Productivity Agent - Tool Execution Node
- File:
apps/backend/swisper/agents/productivity/nodes/productivity_tool_execution_node.py - Function:
execute()method - Node name:
"tool_execution"(checkproductivity_agent.py) -
Ownership:
"tool_execution"⚠️ MUST match node name! -
Wealth Agent - Tool Execution Node
- File:
apps/backend/swisper/agents/wealth/nodes/wealth_tool_execution_node.py - Function:
execute()method - Node name:
"tool_execution"(checkwealth_agent.py) -
Ownership:
"tool_execution"⚠️ MUST match node name! -
Document Agent - Tool Execution Node
- File:
apps/backend/swisper/agents/doc/nodes/doc_tool_execution_node.py - Function:
execute()method - Node name:
"doc_tool_execution_node"(checkdocument_search_agent.py) - Ownership:
"doc_tool_execution_node"⚠️ MUST match node name!
Verification Command:
# Verify all ownership markers match node names
for agent in research_agent productivity_agent wealth_agent doc_agent; do
echo "=== $agent ==="
grep "add_node.*tool" apps/backend/swisper/agents/*$agent*/*.py 2>/dev/null
grep "_tools_executed_by" apps/backend/swisper/agents/*$agent*/nodes/*tool*.py 2>/dev/null
echo
done
# Node names and ownership values should match for each agent
Important Notes:
- ✅ Maintain backward compatibility: Keep existing
tool_resultsfields - ✅ Add, don't replace: Append to existing
_tools_executedlist - ✅ Use ownership markers: Different nodes can use different markers
Verify:
# Send a message that triggers tools
# Example: "Search for Python tutorials"
# Check Redis for tool execution events
redis-cli
XLEN observability:events
# Should show events > 0
# Check SwisperStudio UI for tool traces (if running)
Checklist:
- [ ] Research Agent tool node updated
- [ ] Productivity Agent tool node updated
- [ ] Wealth Agent tool node updated
- [ ] Document Agent tool node updated
Step 7: Update Tests (10 min)¶
Objective: Mock SDK in tests to prevent import errors
Pattern:
In test files that instantiate agents, mock the SDK:
from unittest.mock import patch
from langgraph.graph import StateGraph
# Mock SwisperStudio SDK to prevent import errors in tests
with patch("swisper_studio_sdk.create_traced_graph") as mock_traced_graph:
# Make mock return a standard StateGraph
mock_traced_graph.side_effect = lambda state_class, trace_name: StateGraph(state_class)
# Your test logic here
supervisor = GlobalSupervisor(...)
result = await supervisor.run(...)
Files to Update:
- Any test files that instantiate agents
- Example:
backend/tests/api/services/test_global_supervisor_focused.py
Verify:
# Update Docker container with latest code
docker compose cp apps/backend/swisper/. backend:/app/swisper/
docker compose cp apps/backend/tests/. backend:/app/tests/
# Run tests
docker compose exec backend pytest -vv
# All tests should pass
Troubleshooting:
- ❌ ImportError in tests: Add mock as shown above
- ❌ Tests fail: Check if state schema changes broke existing tests
Step 8: End-to-End Verification (15 min)¶
Objective: Verify full integration works end-to-end
Test Checklist:
-
Backend Starts Successfully
-
Send Test Message
-
Check Redis Events
-
Check SwisperStudio UI (if running)
- Open SwisperStudio UI
- Navigate to Traces
- Should see your test message trace
-
Should see agent nodes and tool executions
-
Verify Graceful Degradation
Success Criteria:
- ✅ Backend starts without errors
- ✅ Redis receives events
- ✅ Agent execution completes normally
- ✅ Traces visible in UI (if running)
- ✅ System works even if SDK/consumer unavailable
✅ Integration Completion Checklist¶
Use this checklist to verify your integration is complete:
Setup & Configuration¶
- [ ] SwisperStudio SDK installed and importable
- [ ] Configuration fields added to
config.py - [ ] Initialization code added to
main.pystartup - [ ] Shutdown code added to
main.pyshutdown - [ ] Redis is running and accessible
State Schemas¶
- [ ] Global Supervisor State updated with
_tools_executedfields - [ ] Research Agent State updated
- [ ] Productivity Agent State updated
- [ ] Wealth Agent State updated
- [ ] Document Agent State updated
- [ ] Type checking passes (
mypy)
Graph Wrappers¶
- [ ] Global Supervisor graph wrapped with
create_traced_graph() - [ ] Research Agent graph wrapped
- [ ] Productivity Agent graph wrapped
- [ ] Wealth Agent graph wrapped
- [ ] Document Agent graph wrapped
- [ ] All agents log "✅ wrapped with SwisperStudio tracing"
Tool Execution Nodes¶
- [ ] Research Agent tool node populates
_tools_executed - [ ] Productivity Agent tool node populates
_tools_executed - [ ] Wealth Agent tool node populates
_tools_executed - [ ] Document Agent tool node populates
_tools_executed - [ ] Ownership markers (
_tools_executed_by) set correctly
Testing¶
- [ ] SDK mocked in test files
- [ ] All existing tests pass
- [ ] Tests run in Docker containers
- [ ] No import errors in tests
Verification¶
- [ ] Backend starts successfully
- [ ] Startup logs show SDK initialization
- [ ] Test message triggers agent execution
- [ ] Redis receives events (
XLEN observability:events > 0) - [ ] Traces visible in SwisperStudio UI (if running)
- [ ] System works even without SDK/consumer (graceful degradation)
Documentation¶
- [ ] Integration documented in team wiki/docs
- [ ] Configuration documented for deployment
- [ ] Troubleshooting guide created (if needed)
⏱️ Estimated Timeline¶
| Step | Duration | Cumulative |
|---|---|---|
| 1. Install SDK | 2 min | 2 min |
| 2. Add Configuration | 5 min | 7 min |
| 3. Add Initialization | 10 min | 17 min |
| 4. Update Agent States | 15 min | 32 min |
| 5. Wrap Graphs | 20 min | 52 min |
| 6. Update Tool Nodes | 30 min | 82 min |
| 7. Update Tests | 10 min | 92 min |
| 8. End-to-End Verification | 15 min | 107 min |
Total: ~1 hour 45 min (for experienced developer familiar with codebase)
🔧 Troubleshooting Common Issues¶
Issue 1: SDK Import Fails¶
Symptom:
Cause: SDK not installed or installed in wrong Python environment
Solution:
# 1. Check if SDK is installed
pip list | grep swisper-studio-sdk
# 2. If not found, install SDK from PyPI
pip install swisper-studio-sdk==0.5.2
# 3. Verify import works
python -c "from swisper_studio_sdk import create_traced_graph; print('✅ SDK works!')"
Still failing?
- Verify you're in the correct Python virtual environment (which python)
- Check pip version: pip --version (should be pip 20+)
- Try upgrading pip: pip install --upgrade pip
- Check PyPI connectivity: pip search swisper or visit https://pypi.org
- See detailed installation in Step 1
Issue 2: Redis Connection Failed¶
Symptom:
Solution:
# Check Redis is running
redis-cli ping
# Should return: PONG
# If not running
docker compose up -d redis
# Check Redis URL in config
python -c "from app.core.config import settings; print(settings.SWISPER_STUDIO_REDIS_URL)"
# Should match your Redis host
Issue 3: No Traces Visible in UI¶
Symptom: Events sent to Redis but not visible in SwisperStudio UI
Solution:
# Check events are in Redis
redis-cli
XLEN observability:events
# Should be > 0
# Check SwisperStudio consumer is running
docker ps | grep swisper_studio
# Check consumer logs
docker logs swisper_studio_consumer
# Verify project ID matches
# Config project ID should match SwisperStudio project
Issue 4: Tests Failing After Integration¶
Symptom:
Solution:
# Add mock to test file
from unittest.mock import patch
from langgraph.graph import StateGraph
with patch("swisper_studio_sdk.create_traced_graph") as mock:
mock.side_effect = lambda state, name: StateGraph(state)
# Your test code
Issue 5: _tools_executed Field Missing¶
Symptom:
Solution:
# Always use .get() with default
existing = state.get("_tools_executed", []) # ✅ Safe
# NOT: state["_tools_executed"] # ❌ Crashes if missing
Issue 6: Tools Not Appearing (No Wrench Icons) 🔧¶
Symptom:
- Tool calls executed successfully
- No wrench icons in SwisperStudio UI
- Tool parameters not visible
- Logs show: ⏭️ Skipping _tools_executed in X: N tools (owned by: Y)
Cause: Ownership marker doesn't match node name
Solution:
# 1. Find the node name in agent build_graph() method
grep "add_node.*tool" apps/backend/swisper/agents/your_agent/your_agent.py
# Example output:
# workflow.add_node("tool_execution", self.tool_execution_node.execute)
# ^^^^^^^^^^^^^^^
# This is your node name!
# 2. Update ownership marker in tool execution node to MATCH
# In apps/backend/swisper/agents/your_agent/nodes/your_tool_execution_node.py:
state["_tools_executed_by"] = "tool_execution" # ✅ Must match node name above!
Verification:
# Check if node names and ownership match
grep "add_node.*tool" apps/backend/swisper/agents/*/your_agent.py
grep "_tools_executed_by" apps/backend/swisper/agents/*/nodes/*tool*.py
# Node name and ownership value should be IDENTICAL
Common Mistakes:
# ❌ WRONG:
workflow.add_node("tool_execution", ...)
state["_tools_executed_by"] = "wealth_tool_execution" # Mismatch!
# ✅ CORRECT:
workflow.add_node("tool_execution", ...)
state["_tools_executed_by"] = "tool_execution" # Match!
# ✅ ALSO CORRECT (if node has unique name):
workflow.add_node("doc_tool_execution_node", ...)
state["_tools_executed_by"] = "doc_tool_execution_node" # Match!
Issue 7: LLM Nodes Showing as PROC/SPAN Instead of LLM 🤖¶
Symptom:
- LLM call nodes (e.g., classify_intent, global_planner) show as PROC (blue) instead of LLM (magenta)
- No prompt/response data visible
- No token counts or costs shown
- "Experiment with Prompt" button missing
Cause: LLM telemetry is not being captured. The SDK auto-detects observation type based on captured LLM data.
Verified Evidence (Dec 2025):
# Checked Redis stream - all observation_end events show:
observation_end type distribution (500 events):
SPAN: 40
NONE: 5
AGENT: 1
GENERATION: 0 ❌ <-- No LLM types being sent!
Quick Fix (Force Type):
# Force GENERATION type for nodes that always make LLM calls
from swisper_studio_sdk import traced
@traced(name="classify_intent", observation_type="GENERATION") # Force LLM type
async def classify_intent(state):
"""This function makes an LLM call, force GENERATION type."""
result = await llm.get_structured_output(...)
return result
Proper Fix (AUTO Detection):
For AUTO detection to work, ensure ALL of these:
-
✅
wrap_llm_adapter()called at startup: -
✅ LLM call uses wrapped adapter:
-
✅ LLM call is INSIDE @traced scope:
-
✅ No silent exceptions: Check logs for any errors during LLM calls that might prevent telemetry capture.
Root Causes (Verified):
1. wrap_llm_adapter() not called → ALL nodes are SPAN
2. LLM call uses different client (e.g., direct OpenAI) → Wrapper doesn't intercept
3. LLM call happens before/after @traced scope → No context to attach telemetry
4. Exception during LLM call → Telemetry not captured
How to Verify:
# Add debug logging:
@traced(name="classify_intent", observation_type="GENERATION")
async def classify_intent(state):
logger.info("🔍 classify_intent: Starting LLM call")
try:
result = await llm_adapter.get_structured_output(...)
logger.info(f"✅ classify_intent: LLM result received")
return result
except Exception as e:
logger.error(f"❌ classify_intent: LLM call failed: {e}")
raise
Check SwisperStudio Consumer: The consumer correctly handles type updates (verified in code):
# observability_consumer.py lines 363-366:
if "type" in data and data.get("type"):
observation.type = data.get("type")
If types are still wrong, the issue is on Swisper/SDK side, not SwisperStudio.
Full Investigation:
See detailed analysis in: docs/investigations/LLM_OBSERVATION_TYPE_INVESTIGATION.md
The @traced Decorator¶
The @traced decorator is used to trace individual functions (not just LangGraph nodes). This is useful for:
- Tracing LLM calls in standalone functions
- Creating nested observations
- Forcing specific observation types
Installation¶
The decorator is included in the SDK:
Basic Usage¶
from swisper_studio_sdk import traced
@traced(name="my_function")
async def my_function(state):
# Your logic here
result = await some_operation()
return result
Decorator Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str | Function name | Name shown in SwisperStudio UI |
observation_type |
str | "AUTO" |
Type: "AUTO", "SPAN", "GENERATION", "TOOL", "AGENT" |
capture_input |
bool | True |
Capture function input in trace |
capture_output |
bool | True |
Capture function output in trace |
Observation Types¶
| Type | Icon | Use Case |
|---|---|---|
AUTO |
🔄 | Default - SDK auto-detects based on LLM data |
SPAN |
📦 | Processing/computation nodes |
GENERATION |
🤖 | LLM calls (prompts/completions) |
TOOL |
🔧 | Tool/function executions |
AGENT |
🧠 | Agent orchestration |
Example: Force LLM Type¶
When AUTO detection doesn't work (e.g., LLM call outside wrapper scope), force the type:
@traced(name="classify_intent", observation_type="GENERATION")
async def classify_intent(state):
"""Force GENERATION type for this LLM-calling function."""
# LLM call happens here
result = await llm.get_structured_output(...)
return result
Example: Nested Observations¶
@traced(name="process_request")
async def process_request(state):
"""Parent observation."""
# Child observation (nested under process_request)
result = await classify_intent(state)
return result
@traced(name="classify_intent", observation_type="GENERATION")
async def classify_intent(state):
"""Child observation - will appear nested in UI."""
return await llm.classify(state)
Example: With State Capture¶
@traced(name="transform_data", capture_input=True, capture_output=True)
async def transform_data(input_state):
"""
Both input and output state will be captured.
Useful for debugging state transformations.
"""
output_state = {**input_state, "transformed": True}
return output_state
LLM Observation Type Detection¶
How AUTO Detection Works¶
The SDK uses a two-phase approach for observation type detection:
Phase 1: observation_start
├── Type: "SPAN" (placeholder)
├── Sent to SwisperStudio
└── Observation created in DB
Phase 2: observation_end
├── Type: "GENERATION" (if LLM data captured) OR "SPAN" (default)
├── Sent to SwisperStudio
└── Observation type UPDATED in DB
Detection Logic¶
# Inside SDK @traced decorator:
def _detect_observation_type(name, has_llm_data, has_tool_data):
if has_llm_data:
return "GENERATION" # LLM call detected!
elif has_tool_data:
return "TOOL"
elif name in KNOWN_AGENT_NAMES:
return "AGENT"
else:
return "SPAN" # Default fallback
What Sets has_llm_data = True?¶
The SDK's LLM wrapper (wrap_llm_adapter()) intercepts LLM calls and sets this flag.
For AUTO detection to work, ALL of these must be true:
- ✅
wrap_llm_adapter()called at startup - ✅ LLM call uses
TokenTrackingLLMAdapter.get_structured_output() - ✅ LLM call happens INSIDE the
@traceddecorated function scope - ✅ No exceptions during LLM call
Why LLM Nodes Show as SPAN (Troubleshooting)¶
If your LLM nodes show as SPAN instead of GENERATION:
| Symptom | Cause | Fix |
|---|---|---|
| All nodes are SPAN | wrap_llm_adapter() not called |
Add to startup code |
| Specific node is SPAN | LLM call outside @traced scope |
Move LLM call inside decorated function |
| Specific node is SPAN | Different LLM client used | Use TokenTrackingLLMAdapter |
| Specific node is SPAN | LLM call failed/raised exception | Check for silent failures |
Quick Fix: Force GENERATION Type¶
If AUTO detection doesn't work, explicitly set the type:
# Instead of:
@traced(name="classify_intent") # ❌ May detect as SPAN
# Use:
@traced(name="classify_intent", observation_type="GENERATION") # ✅ Force LLM type
Verification: Check What SDK Sends¶
Add debug logging to verify what type is being sent:
# In Swisper backend, add to your LLM-calling function:
import logging
logger = logging.getLogger(__name__)
@traced(name="classify_intent", observation_type="GENERATION")
async def classify_intent(state):
logger.info("📤 classify_intent: observation_type=GENERATION (forced)")
result = await llm.get_structured_output(...)
logger.info(f"📥 classify_intent: result received")
return result
SwisperStudio Consumer Handling¶
The consumer DOES handle type updates correctly (verified in code):
# apps/backend/swisper/services/observability_consumer.py, lines 363-366:
# Update type if provided (for AUTO type detection)
if "type" in data and data.get("type"):
observation.type = data.get("type")
If types are still wrong, the issue is on the Swisper/SDK side, not SwisperStudio.
🎓 Next Steps After Integration¶
Once integration is complete:
- Monitor Performance
- Check Redis memory usage
- Monitor event processing latency
-
Verify no significant overhead
-
Team Training
- Train team on SwisperStudio UI
- Document how to use traces for debugging
-
Create runbook for common scenarios
-
Expand Observability
- Add custom metrics to
_tools_executed - Implement cost tracking
-
Add performance metrics
-
Configuration Management
- Plan prompt management via UI
- Design A/B testing framework
- Define configuration workflows
Integration Summary¶
What Was Changed¶
The SwisperStudio SDK v0.5.2 integration involved changes across three main areas:
- Agent State Schemas - Added standardized
_tools_executedfield - Graph Creation - Wrapped StateGraph with
create_traced_graph() - Tool Execution Nodes - Populated
_tools_executedformat in all tool execution nodes
Design Philosophy¶
- ✅ Non-invasive - Uses decorator pattern, no business logic changes
- ✅ Graceful degradation - Falls back to standard StateGraph if SDK unavailable
- ✅ Standardized format - All agents use same
_tools_executedstructure - ✅ Backward compatible - Maintains existing
tool_resultsformats
State Changes¶
Standard Format (v0.5.2)¶
All agent states now include two standardized fields for observability:
_tools_executed: List[Dict[str, Any]] # STANDARD FORMAT: Tool execution trace
_tools_executed_by: Optional[str] # OWNERSHIP MARKER: Which node created this
Format Structure:
{
"tool_name": str, # Tool/function name
"parameters": dict, # Input parameters
"result": Any, # Execution result
"timestamp": str, # ISO 8601 timestamp
"status": str # "success" | "failure"
}
Changed Files¶
All agent state files were updated:
1. Global Supervisor State¶
File: apps/backend/swisper/models/state/global_supervisor.py
class GlobalSupervisorState(TypedDict):
# ... existing fields ...
# NEW: SwisperStudio observability fields
_tools_executed: Optional[List[Dict[str, Any]]]
_tools_executed_by: Optional[str]
Purpose: Aggregated tool executions from all domain agents
2. Research Agent State¶
File: apps/backend/swisper/models/state/research.py
class ResearchAgentState(TypedDict):
# ... existing fields ...
tool_execution_results_history: List[ToolExecutionResult] # Backwards compat
# NEW: SwisperStudio observability
_tools_executed: List[Dict[str, Any]]
_tools_executed_by: Optional[str]
iteration_count: int
Purpose: Tracks web search and research tool executions
3. Productivity Agent State¶
File: apps/backend/swisper/models/state/productivity.py
class ProductivityAgentState(TypedDict):
# ... existing fields ...
tool_results: Dict[str, Any] # Backwards compat format
# NEW: SwisperStudio observability
_tools_executed: List[Dict[str, Any]]
_tools_executed_by: Optional[str]
final_result: Optional[str]
Purpose: Tracks calendar, email, contact, task tool executions
4. Wealth Agent State¶
File: apps/backend/swisper/models/state/wealth.py
class WealthAgentState(TypedDict):
# ... existing fields ...
tool_execution_result: Optional[Any] # Backwards compat
# NEW: SwisperStudio observability
_tools_executed: List[Dict[str, Any]]
_tools_executed_by: Optional[str]
wealth_planner_result: Optional[Any]
Purpose: Tracks financial data retrieval tool executions
5. Document Agent State¶
File: apps/backend/swisper/models/state/doc.py
class DocumentSearchAgentState(TypedDict):
# ... existing fields ...
tool_results: Dict[str, str] # Backwards compat format
# NEW: SwisperStudio observability
_tools_executed: List[Dict[str, Any]]
_tools_executed_by: Optional[str]
tools: List[DocAgentToolInfo]
available_documents: List[DocumentInfo]
Purpose: Tracks document search and RAG tool executions
Graph Wrapper Pattern¶
Pattern Overview¶
All agents now use create_traced_graph() wrapper instead of direct StateGraph instantiation:
# ❌ OLD: Direct StateGraph
workflow = StateGraph(AgentState)
# ✅ NEW: Traced graph with fallback
try:
from swisper_studio_sdk import create_traced_graph
workflow = create_traced_graph(
AgentState,
trace_name="agent_name"
)
logger.info("✅ Agent graph wrapped with SwisperStudio tracing")
except ImportError:
workflow = StateGraph(AgentState)
logger.warning("⚠️ SwisperStudio SDK not available - tracing disabled")
Implementation Per Agent¶
1. Global Supervisor¶
File: apps/backend/swisper/agents/supervisor/agent.py
Lines: 175-186
# Initialize graph with SwisperStudio tracing (if SDK available)
try:
from swisper_studio_sdk import create_traced_graph
graph = create_traced_graph(
GlobalSupervisorState,
trace_name="global_supervisor"
)
self.logger.info("✅ GlobalSupervisor graph wrapped with SwisperStudio tracing")
except ImportError:
# Fallback to standard StateGraph if SDK not available
graph = StateGraph(GlobalSupervisorState)
self.logger.debug("Using standard StateGraph (SwisperStudio SDK not available)")
2. Research Agent¶
File: apps/backend/swisper/agents/research/agent.py
Lines: 269-279
# Create workflow graph with SwisperStudio tracing
try:
from swisper_studio_sdk import create_traced_graph
workflow = create_traced_graph(
ResearchAgentState,
trace_name="research_agent"
)
self.logger.info("✅ ResearchAgent graph wrapped with SwisperStudio tracing")
except ImportError:
workflow = StateGraph(ResearchAgentState)
self.logger.warning("⚠️ SwisperStudio SDK not available - ResearchAgent tracing disabled")
3. Productivity Agent¶
File: apps/backend/swisper/agents/productivity/agent.py
Lines: 141-151
# Create workflow graph with optimized state and SwisperStudio tracing
try:
from swisper_studio_sdk import create_traced_graph
workflow = create_traced_graph(
ProductivityAgentState,
trace_name="productivity_agent"
)
self.logger.info("✅ ProductivityAgent graph wrapped with SwisperStudio tracing")
except ImportError:
workflow = StateGraph(ProductivityAgentState)
self.logger.warning("⚠️ SwisperStudio SDK not available - ProductivityAgent tracing disabled")
4. Wealth Agent¶
File: apps/backend/swisper/agents/wealth/agent.py
Lines: 236-246
# Create workflow graph with SwisperStudio tracing
try:
from swisper_studio_sdk import create_traced_graph
workflow = create_traced_graph(
WealthAgentState,
trace_name="wealth_agent"
)
self.logger.info("✅ WealthAgent graph wrapped with SwisperStudio tracing")
except ImportError:
workflow = StateGraph(WealthAgentState)
self.logger.warning("⚠️ SwisperStudio SDK not available - WealthAgent tracing disabled")
5. Document Agent¶
File: apps/backend/swisper/agents/doc/agent.py
Lines: 108-118
# Create workflow graph with SwisperStudio tracing
try:
from swisper_studio_sdk import create_traced_graph
workflow = create_traced_graph(
DocumentSearchAgentState,
trace_name="document_search_agent"
)
self.logger.info("✅ DocumentSearchAgent graph wrapped with SwisperStudio tracing")
except ImportError:
workflow = StateGraph(DocumentSearchAgentState)
self.logger.warning("⚠️ SwisperStudio SDK not available - DocumentSearchAgent tracing disabled")
Tool Execution Standardization¶
All tool execution nodes were updated to populate _tools_executed in the standardized format.
Standard Pattern¶
# STEP 1: Execute tool (existing logic)
result = await execute_tool(params)
# STEP 2: Create standardized tool entry
tool_entry = {
"tool_name": "tool_name",
"parameters": params,
"result": result,
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "success" if success else "failure"
}
# STEP 3: Append to _tools_executed
existing_tools_executed = state.get("_tools_executed", [])
updated_tools_executed = existing_tools_executed + [tool_entry]
# STEP 4: Update state with ownership marker
state["_tools_executed"] = updated_tools_executed
state["_tools_executed_by"] = "node_name" # Prevents duplicate extraction
Changed Files¶
1. Research Agent - Tool Execution Node¶
File: apps/backend/swisper/agents/research/nodes/tool_execution_node.py
Lines: 126-159
# STANDARDIZATION: Initialize _tools_executed for new standard format
tools_executed_standard = []
# ... execute tools ...
# Add to standard format
tools_executed_standard.append({
"tool_name": operation.tool_name,
"parameters": operation.parameters,
"result": tool_result,
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "success" if success else "failure"
})
# Get existing _tools_executed or initialize
existing_tools_executed = state.get("_tools_executed", [])
updated_tools_executed = existing_tools_executed + tools_executed_standard
return {
**state,
"tool_execution_results_history": updated_history,
"_tools_executed": updated_tools_executed, # NEW STANDARD FORMAT
"_tools_executed_by": "tool_execution" # OWNERSHIP MARKER
}
2. Productivity Agent - Tool Execution Node¶
File: apps/backend/swisper/agents/productivity/nodes/productivity_tool_execution_node.py
Lines: 278-346
"""
STANDARDIZATION (v0.5.2): Now populates both formats:
- tool_results: Dict (backwards compat)
- _tools_executed: List[Dict] (new standard)
"""
# STANDARDIZATION: Convert to new _tools_executed format
existing_tools_executed = state.get("_tools_executed", [])
new_tools_executed = []
for result_key, result_data in execution_results.items():
if isinstance(result_data, dict):
new_tools_executed.append({
"tool_name": result_data.get("tool_name", "unknown"),
"parameters": result_data.get("tool_args", {}),
"result": result_data.get("result"),
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "success" if result_data.get("success") else "failure"
})
updated_tools_executed = existing_tools_executed + new_tools_executed
updated_state = {
**state,
"tool_results": updated_tool_results,
"_tools_executed": updated_tools_executed, # NEW STANDARD FORMAT
"_tools_executed_by": "tool_execution", # OWNERSHIP MARKER
}
3. Wealth Agent - Tool Execution Node¶
File: apps/backend/swisper/agents/wealth/nodes/wealth_tool_execution_node.py
Lines: 122-150
# STANDARDIZATION (v0.5.2): Populate _tools_executed format
tools_executed_standard = []
for operation in wealth_planner_result.tool_operations:
tool_result = results.get(operation.operation_id)
success = tool_result and not isinstance(tool_result, Exception)
tools_executed_standard.append({
"tool_name": operation.tool_name,
"parameters": operation.parameters,
"result": str(tool_result) if tool_result else None,
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "success" if success else "failure"
})
existing_tools_executed = state.get("_tools_executed", [])
updated_tools_executed = existing_tools_executed + tools_executed_standard
return {
**state,
"tool_execution_result": tool_execution_result,
"_tools_executed": updated_tools_executed, # NEW STANDARD FORMAT
"_tools_executed_by": "wealth_tool_execution", # OWNERSHIP MARKER
}
4. Document Agent - Tool Execution Node¶
File: apps/backend/swisper/agents/doc/nodes/doc_tool_execution_node.py
Lines: 20-85
"""
STANDARDIZATION (v0.5.2): Populates both tool_results (backwards compat)
and _tools_executed (new standard format).
"""
# Execute tool (existing logic)
result = await tool_executor(current_plan.tool_input)
# Create standardized tool entry
tool_entry = {
"tool_name": current_plan.tool,
"parameters": {"input": current_plan.tool_input},
"result": result,
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "success" if result else "failure"
}
# STANDARDIZATION: Add to _tools_executed
existing_tools_executed = state.get("_tools_executed", [])
updated_tools_executed = existing_tools_executed + [tool_entry]
state["_tools_executed"] = updated_tools_executed
state["_tools_executed_by"] = "doc_tool_execution" # OWNERSHIP MARKER
Configuration¶
Config File¶
File: apps/backend/swisper/core/config.py
Lines: 221-232
# SwisperStudio Integration (Observability & Configuration Management)
# SDK v0.5.2: Redis Streams-based observability (50x faster than HTTP)
SWISPER_STUDIO_ENABLED: bool = True
SWISPER_STUDIO_REDIS_URL: str = "redis://redis:6379" # Redis for observability events
SWISPER_STUDIO_STREAM_NAME: str = "observability:events"
SWISPER_STUDIO_PROJECT_ID: str = "0d7aa606-cb29-4a31-8a59-50fa61151a32"
SWISPER_STUDIO_CAPTURE_REASONING: bool = True # Capture LLM reasoning (<think> tags)
SWISPER_STUDIO_REASONING_MAX_LENGTH: int = 50000 # 50 KB limit for reasoning
Configuration Fields¶
| Field | Type | Default | Purpose |
|---|---|---|---|
SWISPER_STUDIO_ENABLED |
bool | True |
Enable/disable observability |
SWISPER_STUDIO_REDIS_URL |
str | redis://redis:6379 |
Redis connection for events |
SWISPER_STUDIO_STREAM_NAME |
str | observability:events |
Redis Stream name |
SWISPER_STUDIO_PROJECT_ID |
str | UUID | Project identifier |
SWISPER_STUDIO_CAPTURE_REASONING |
bool | True |
Capture <think> tags |
SWISPER_STUDIO_REASONING_MAX_LENGTH |
int | 50000 |
Max reasoning length (bytes) |
Initialization¶
Application Startup¶
File: apps/backend/app/main.py
Lines: 91-127
# Initialize SwisperStudio observability (SDK v0.5.2 - Q2: Tracing Toggle)
if settings.SWISPER_STUDIO_ENABLED:
try:
from swisper_studio_sdk import initialize_redis_publisher, wrap_llm_adapter
# Initialize Redis Streams publisher for observability
await initialize_redis_publisher(
redis_url=settings.SWISPER_STUDIO_REDIS_URL,
stream_name=settings.SWISPER_STUDIO_STREAM_NAME,
project_id=settings.SWISPER_STUDIO_PROJECT_ID,
verify_consumer=True, # Check if SwisperStudio consumer is running
)
logger.info("✅ SwisperStudio observability initialized (Redis Streams)")
logger.info(f" Redis: {settings.SWISPER_STUDIO_REDIS_URL}")
logger.info(f" Stream: {settings.SWISPER_STUDIO_STREAM_NAME}")
logger.info(f" Project ID: {settings.SWISPER_STUDIO_PROJECT_ID}")
# Enable LLM prompt capture (includes streaming support)
try:
wrap_llm_adapter()
logger.info("✅ LLM prompt capture enabled (structured + streaming)")
except Exception as e:
logger.warning(f"⚠️ LLM prompt capture disabled: {e}")
logger.warning(" State capture will still work")
except ImportError:
logger.warning("⚠️ SwisperStudio SDK not installed - observability disabled")
logger.warning(" Install with: pip install swisper-studio-sdk==0.5.2")
except Exception as e:
logger.warning(f"⚠️ SwisperStudio observability initialization failed: {e}")
logger.warning(" Continuing without observability - events will queue in Redis")
Shutdown Handler¶
File: apps/backend/app/main.py
Lines: 155-161
# Close SwisperStudio Redis publisher
if settings.SWISPER_STUDIO_ENABLED:
try:
from swisper_studio_sdk import close_redis_publisher
await close_redis_publisher()
logger.info("✅ SwisperStudio Redis publisher closed")
except Exception as e:
logger.warning(f"⚠️ Failed to close SwisperStudio publisher: {e}")
Initialization Sequence¶
- Check if enabled -
settings.SWISPER_STUDIO_ENABLED - Import SDK functions -
initialize_redis_publisher,wrap_llm_adapter - Initialize Redis publisher - Connect to Redis Streams
- Verify consumer running - Check SwisperStudio consumer is alive
- Wrap LLM adapter - Enable prompt/response capture
- Graceful degradation - Continue if SDK unavailable or initialization fails
Testing¶
Test Updates¶
File: backend/tests/api/services/test_global_supervisor_focused.py
Lines: 193-209
Mock the SDK for tests:
# Mock SwisperStudio SDK to prevent import errors in tests
with patch("swisper_studio_sdk.create_traced_graph") as mock_traced_graph:
# Make mock return a standard StateGraph
mock_traced_graph.side_effect = lambda state_class, trace_name: StateGraph(state_class)
# Your test logic here
supervisor = GlobalSupervisor(...)
result = await supervisor.run(...)
Testing Checklist¶
- [ ] All agents initialize successfully with SDK
- [ ] All agents fall back gracefully without SDK
- [ ]
_tools_executedpopulated correctly in all agents - [ ] Redis publisher initializes on startup
- [ ] Redis publisher closes on shutdown
- [ ] LLM wrapper captures prompts/responses
- [ ] Tests pass with mocked SDK
Backward Compatibility¶
Dual Format Strategy¶
All tool execution nodes maintain both formats:
- Legacy format - Existing
tool_results,tool_execution_resultfields - Standard format - New
_tools_executedfield
Example:
return {
**state,
# LEGACY: Backward compatible format
"tool_results": updated_tool_results,
# NEW: Standard observability format
"_tools_executed": updated_tools_executed,
"_tools_executed_by": "tool_execution"
}
Why Both Formats?¶
- ✅ Gradual migration - Existing code continues to work
- ✅ No breaking changes - UI/API consumers unaffected
- ✅ Future deprecation - Can remove legacy format later
- ✅ Observability parallel - New format for tracing only
Ownership Marker Pattern¶
Purpose¶
The _tools_executed_by field serves two critical functions:
- Prevents duplicate extraction - Only the originating node populates the field
- Enables tool observation creation - SDK extracts tools ONLY when node name matches ownership marker
⚠️ CRITICAL REQUIREMENT: Match Node Name¶
The ownership marker MUST EXACTLY match the node name in add_node().
How the SDK Checks:
# In SDK decorator (internal):
created_by = output.get('_tools_executed_by')
is_owner = (created_by == node_name) # EXACT match required!
if is_owner:
# Extract tools and create wrench icons 🔧
else:
# Skip tool extraction (no wrench icons!)
Correct Ownership Values¶
Find your node name and use it as the ownership value:
| Agent | Node Name (in add_node()) |
Ownership Value (_tools_executed_by) |
Status |
|---|---|---|---|
| Research Agent | "tool_execution" |
"tool_execution" |
✅ Correct |
| Productivity Agent | "tool_execution" |
"tool_execution" |
✅ Correct |
| Wealth Agent | "tool_execution" |
"tool_execution" |
✅ Correct |
| Document Agent | "doc_tool_execution_node" |
"doc_tool_execution_node" |
✅ Correct |
How to Verify¶
Step 1: Find node name in agent file:
grep "add_node.*tool" apps/backend/swisper/agents/wealth/agent.py
# Output: workflow.add_node("tool_execution", self.tool_execution_node.execute)
# ^^^^^^^^^^^^^^^
# Your node name
Step 2: Check ownership marker in tool execution node:
grep "_tools_executed_by" apps/backend/swisper/agents/wealth/nodes/wealth_tool_execution_node.py
# Should show: state["_tools_executed_by"] = "tool_execution" # MUST MATCH!
Step 3: Verify they match:
- Node name: "tool_execution"
- Ownership: "tool_execution"
- ✅ Match = Tools will appear
Why Needed?¶
- ✅ Prevents double-counting - Only the originating node populates the field
- ✅ Clear attribution - Know which node created the tool trace
- ✅ Debugging aid - Trace tool execution back to source node
- ✅ Enables UI display - SDK won't extract tools if ownership doesn't match!
SDK Installation¶
⚠️ Important: For detailed SDK installation instructions, see Step 1: Install SwisperStudio SDK above.
Quick Reference¶
Package Name: swisper-studio-sdk
Current Version: 0.5.2
Distribution: PyPI (Public Package Index)
PyPI URL: https://pypi.org/project/swisper-studio-sdk/
Installation Command (One-Line)¶
Alternative Installation Methods¶
# Method 1: From Git + Tag (if you need unreleased changes)
pip install git+https://github.com/Fintama/swisper_studio.git@sdk-v0.5.2#subdirectory=sdk
# Method 2: From local clone (for SDK development)
pip install -e /path/to/swisper_studio/sdk
# Method 3: In requirements.txt
# swisper-studio-sdk==0.5.2
No Authentication Required¶
The SDK is published on public PyPI - just pip install and go!
For SDK Developers Only¶
Only use this if you're actively developing the SDK itself:
cd backend
pip install -e /path/to/swisper_studio/sdk
# Or with uv
uv pip install -e /path/to/swisper_studio/sdk
This installs the SDK in "editable" mode, allowing you to make changes to the SDK code and see them immediately without reinstalling.
Summary of Changes¶
Files Modified¶
| Category | File | Changes |
|---|---|---|
| Config | app/core/config.py |
Added SwisperStudio config fields |
| Startup | app/main.py |
Added SDK initialization + shutdown |
| Global Supervisor | agents/global_supervisor_state.py |
Added _tools_executed fields |
| Global Supervisor | agents/global_supervisor/global_supervisor.py |
Wrapped graph with create_traced_graph() |
| Research Agent | agents/research_agent/agent_state.py |
Added _tools_executed fields |
| Research Agent | agents/research_agent/research_agent.py |
Wrapped graph with create_traced_graph() |
| Research Agent | agents/research_agent/nodes/tool_execution_node.py |
Populate _tools_executed |
| Productivity Agent | agents/productivity_agent/productivity_agent_state.py |
Added _tools_executed fields |
| Productivity Agent | agents/productivity_agent/productivity_agent.py |
Wrapped graph with create_traced_graph() |
| Productivity Agent | agents/productivity_agent/nodes/productivity_tool_execution_node.py |
Populate _tools_executed |
| Wealth Agent | agents/wealth_agent/agent_state.py |
Added _tools_executed fields |
| Wealth Agent | agents/wealth_agent/wealth_agent.py |
Wrapped graph with create_traced_graph() |
| Wealth Agent | agents/wealth_agent/nodes/wealth_tool_execution_node.py |
Populate _tools_executed |
| Document Agent | agents/doc_agent/document_state.py |
Added _tools_executed fields |
| Document Agent | agents/doc_agent/document_search_agent.py |
Wrapped graph with create_traced_graph() |
| Document Agent | agents/doc_agent/nodes/doc_tool_execution_node.py |
Populate _tools_executed |
| Tests | tests/api/services/test_global_supervisor_focused.py |
Mock SDK for tests |
Total: 17 files modified
Architecture Diagram¶
┌─────────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Startup (main.py) │ │
│ │ 1. Initialize Redis publisher │ │
│ │ 2. Wrap LLM adapter │ │
│ │ 3. Verify consumer running │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Agents (LangGraph Workflows) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Global Supervisor │ │ │
│ │ │ • create_traced_graph(GlobalSupervisorState)│ │ │
│ │ │ • _tools_executed: aggregated from domains │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Domain Agents │ │ │
│ │ │ • create_traced_graph(AgentState) │ │ │
│ │ │ • Tool nodes populate _tools_executed │ │ │
│ │ │ • Ownership marker: _tools_executed_by │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SwisperStudio SDK (v0.5.2) │ │
│ │ • create_traced_graph() - Wraps StateGraph │ │
│ │ • Captures state transitions │ │
│ │ • Extracts _tools_executed │ │
│ │ • Publishes to Redis Streams │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────┼────────────────────────────────┘
│
▼
┌──────────────────┐
│ Redis Streams │
│ observability: │
│ events │
└──────────────────┘
│
▼
┌──────────────────┐
│ SwisperStudio UI │
│ (Consumer) │
└──────────────────┘
Next Steps¶
Immediate¶
- ✅ Integration complete
- ✅ All agents instrumented
- ✅ Backward compatibility maintained
Future Enhancements¶
- [ ] Add custom metrics to
_tools_executed(latency, token counts) - [ ] Implement configuration management via SwisperStudio UI
- [ ] Add A/B testing framework for prompt variants
- [ ] Expand observability to include cost tracking
Related Documentation¶
- Spec:
docs/specs/prompt_studio_spec.md - Plan:
docs/plans/swisper_studio_implementation_plan.md - Architecture:
docs/Documentation/SWISPER_ARCHITECTURE.md - Agent Creation:
docs/guides/agent_guides/agent_creation_guide.md - LLM Observation Type Investigation:
docs/investigations/LLM_OBSERVATION_TYPE_INVESTIGATION.md⭐ NEW
Questions?¶
Contact the development team or see the SwisperStudio SDK documentation.
Last Updated: December 4, 2025 (v1.4) Maintained By: Swisper Backend Team
Quick Reference Card¶
SDK Installation¶
Import Statement¶
from swisper_studio_sdk import (
create_traced_graph, # Wrap LangGraph StateGraph
traced, # Decorator for individual functions
initialize_redis_publisher, # Start Redis publisher
close_redis_publisher, # Stop Redis publisher
wrap_llm_adapter, # Enable LLM telemetry capture
)
Decorator Cheat Sheet¶
# Basic tracing
@traced(name="my_function")
# Force LLM type (for LLM-calling functions)
@traced(name="classify_intent", observation_type="GENERATION")
# Tool execution
@traced(name="execute_search", observation_type="TOOL")
# Agent orchestration
@traced(name="supervisor", observation_type="AGENT")
# Disable input/output capture (for sensitive data)
@traced(name="process_secrets", capture_input=False, capture_output=False)