Skip to content

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 @traced decorator 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 @traced scope
  • 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

  1. Step-by-Step Integration TutorialSTART HERE
  2. The @traced DecoratorNEW
  3. LLM Observation Type DetectionNEW
  4. Integration Summary
  5. State Changes
  6. Graph Wrapper Pattern
  7. Tool Execution Standardization
  8. Configuration
  9. Initialization
  10. Testing
  11. 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

cd /path/to/swisper_studio
docker compose up -d

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:

pip install swisper-studio-sdk==0.5.2

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:

# requirements.txt

# SwisperStudio SDK from PyPI
swisper-studio-sdk==0.5.2

Then install all dependencies:

pip install -r requirements.txt

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:

swisper-studio-sdk==0.5.2

Build and run:

docker compose build backend
docker compose up -d backend

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:

✅ SDK installed successfully!


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:

  1. Open SwisperStudio UI (development: http://localhost:3000, production: https://swisper-studio.fintama.com)
  2. Navigate to "Projects"
  3. Create a new project (e.g., "Swisper Production" or "Swisper Development")
  4. Click on the project
  5. Copy the UUID from the URL: /projects/{THIS_IS_YOUR_PROJECT_ID}/...
  6. Paste into SWISPER_STUDIO_PROJECT_ID field

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_URL is 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:

  1. Global Supervisor State
  2. File: apps/backend/swisper/models/state/global_supervisor.py
  3. Type: Optional[List[Dict[str, Any]]] (aggregates from all agents)

  4. Research Agent State

  5. File: apps/backend/swisper/models/state/research.py
  6. Type: List[Dict[str, Any]]

  7. Productivity Agent State

  8. File: apps/backend/swisper/models/state/productivity.py
  9. Type: List[Dict[str, Any]]

  10. Wealth Agent State

  11. File: apps/backend/swisper/models/state/wealth.py
  12. Type: List[Dict[str, Any]]

  13. Document Agent State

  14. File: apps/backend/swisper/models/state/doc.py
  15. 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:

  1. Global Supervisor
  2. File: apps/backend/swisper/agents/supervisor/agent.py
  3. Location: In build_graph() method (around line 175)
  4. Trace name: "global_supervisor"

  5. Research Agent

  6. File: apps/backend/swisper/agents/research/agent.py
  7. Location: In build_graph() method (around line 269)
  8. Trace name: "research_agent"

  9. Productivity Agent

  10. File: apps/backend/swisper/agents/productivity/agent.py
  11. Location: In build_graph() method (around line 141)
  12. Trace name: "productivity_agent"

  13. Wealth Agent

  14. File: apps/backend/swisper/agents/wealth/agent.py
  15. Location: In build_graph() method (around line 236)
  16. Trace name: "wealth_agent"

  17. Document Agent

  18. File: apps/backend/swisper/agents/doc/agent.py
  19. Location: In build_graph() method (around line 108)
  20. 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:

  1. Research Agent - Tool Execution Node
  2. File: apps/backend/swisper/agents/research/nodes/tool_execution_node.py
  3. Function: execute() method
  4. Node name: "tool_execution" (check research_agent.py)
  5. Ownership: "tool_execution" ⚠️ MUST match node name!

  6. Productivity Agent - Tool Execution Node

  7. File: apps/backend/swisper/agents/productivity/nodes/productivity_tool_execution_node.py
  8. Function: execute() method
  9. Node name: "tool_execution" (check productivity_agent.py)
  10. Ownership: "tool_execution" ⚠️ MUST match node name!

  11. Wealth Agent - Tool Execution Node

  12. File: apps/backend/swisper/agents/wealth/nodes/wealth_tool_execution_node.py
  13. Function: execute() method
  14. Node name: "tool_execution" (check wealth_agent.py)
  15. Ownership: "tool_execution" ⚠️ MUST match node name!

  16. Document Agent - Tool Execution Node

  17. File: apps/backend/swisper/agents/doc/nodes/doc_tool_execution_node.py
  18. Function: execute() method
  19. Node name: "doc_tool_execution_node" (check document_search_agent.py)
  20. 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_results fields
  • Add, don't replace: Append to existing _tools_executed list
  • 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:

  1. Backend Starts Successfully

    fastapi run app/main.py
    
    # Check logs for:
    # ✅ SwisperStudio observability initialized (Redis Streams)
    # ✅ LLM prompt capture enabled
    # ✅ All agent graphs wrapped with SwisperStudio tracing
    

  2. Send Test Message

    # Use your API client or frontend
    # Send message: "Search for Python tutorials"
    

  3. Check Redis Events

    redis-cli
    XLEN observability:events
    # Should show events (number > 0)
    
    # Read latest event
    XREVRANGE observability:events + - COUNT 1
    # Should show JSON event data
    

  4. Check SwisperStudio UI (if running)

  5. Open SwisperStudio UI
  6. Navigate to Traces
  7. Should see your test message trace
  8. Should see agent nodes and tool executions

  9. Verify Graceful Degradation

    # Stop SwisperStudio consumer (if running)
    # Backend should continue working
    # Logs should show warnings but no crashes
    

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.py startup
  • [ ] Shutdown code added to main.py shutdown
  • [ ] Redis is running and accessible

State Schemas

  • [ ] Global Supervisor State updated with _tools_executed fields
  • [ ] 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:

ImportError: No module named 'swisper_studio_sdk'

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:

⚠️ SwisperStudio observability initialization failed: Connection refused

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:

ImportError: cannot import name 'create_traced_graph' from 'swisper_studio_sdk'

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:

KeyError: '_tools_executed'

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:

  1. wrap_llm_adapter() called at startup:

    # In main.py startup:
    from swisper_studio_sdk import wrap_llm_adapter
    wrap_llm_adapter()
    logger.info("✅ LLM prompt capture enabled")  # Must see this log!
    

  2. LLM call uses wrapped adapter:

    # Use TokenTrackingLLMAdapter.get_structured_output()
    result = await llm_adapter.get_structured_output(...)
    

  3. LLM call is INSIDE @traced scope:

    @traced(name="classify_intent")
    async def classify_intent(state):
        # LLM call MUST be here, inside the decorated function
        result = await llm_adapter.get_structured_output(...)  # ✅ Inside scope
        return result
    
    # ❌ WRONG: LLM call outside decorated function won't be detected
    

  4. 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:

from swisper_studio_sdk import traced

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:

  1. wrap_llm_adapter() called at startup
  2. ✅ LLM call uses TokenTrackingLLMAdapter.get_structured_output()
  3. ✅ LLM call happens INSIDE the @traced decorated function scope
  4. ✅ 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:

  1. Monitor Performance
  2. Check Redis memory usage
  3. Monitor event processing latency
  4. Verify no significant overhead

  5. Team Training

  6. Train team on SwisperStudio UI
  7. Document how to use traces for debugging
  8. Create runbook for common scenarios

  9. Expand Observability

  10. Add custom metrics to _tools_executed
  11. Implement cost tracking
  12. Add performance metrics

  13. Configuration Management

  14. Plan prompt management via UI
  15. Design A/B testing framework
  16. Define configuration workflows

Integration Summary

What Was Changed

The SwisperStudio SDK v0.5.2 integration involved changes across three main areas:

  1. Agent State Schemas - Added standardized _tools_executed field
  2. Graph Creation - Wrapped StateGraph with create_traced_graph()
  3. Tool Execution Nodes - Populated _tools_executed format 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_executed structure
  • Backward compatible - Maintains existing tool_results formats

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

  1. Check if enabled - settings.SWISPER_STUDIO_ENABLED
  2. Import SDK functions - initialize_redis_publisher, wrap_llm_adapter
  3. Initialize Redis publisher - Connect to Redis Streams
  4. Verify consumer running - Check SwisperStudio consumer is alive
  5. Wrap LLM adapter - Enable prompt/response capture
  6. 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_executed populated 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:

  1. Legacy format - Existing tool_results, tool_execution_result fields
  2. Standard format - New _tools_executed field

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:

  1. Prevents duplicate extraction - Only the originating node populates the field
  2. Enables tool observation creation - SDK extracts tools ONLY when node name matches ownership marker
state["_tools_executed_by"] = "tool_execution"  # Mark ownership

⚠️ 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)

# Install from PyPI (no authentication needed)
pip install swisper-studio-sdk==0.5.2

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

  • 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

pip install swisper-studio-sdk==0.5.2

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)

Graph Wrapper

from swisper_studio_sdk import create_traced_graph

workflow = create_traced_graph(MyAgentState, trace_name="my_agent")