Skip to main content

Overview

CopilotKit is a WebSocket-based agent integration pattern that provides real-time, bidirectional communication between your React frontend and Python backend. It’s ideal for building chat interfaces and contextual help features with minimal setup.

When to Use CopilotKit

Best suited for:
  • Simple chat interfaces with real-time streaming
  • Contextual help and in-app assistance
  • Quick Q&A features with instant responses
  • Real-time collaboration features
  • Single-agent conversations
Not recommended for:
  • Complex multi-agent orchestration (use AG-UI Protocol instead)
  • Workflows requiring persistent state across sessions (use Workspace Context Sync)
  • Bulk data processing or long-running background tasks

Architecture

CopilotKit uses WebSocket connections for low-latency, bidirectional communication: Key Benefits:
  • Low latency: WebSocket maintains persistent connection for instant responses
  • Bidirectional: Server can push updates to client without polling
  • Streaming: Real-time token streaming as the AI generates responses
  • Simple state: Client-side state management with React hooks

Backend Setup

1. Install Dependencies

Add CopilotKit to your Python backend:
pip install copilotkit fastapi
Your requirements.txt should include:
copilotkit>=1.0.0
fastapi>=0.104.0
uvicorn[standard]>=0.24.0

2. Create CopilotKit Endpoint

Create a new file app/api/copilot.py:
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
from copilotkit import CopilotKitSDK, LangGraphAgent
from app.agent.graph import create_agent_graph
from typing import Optional
import logging

logger = logging.getLogger(__name__)
router = APIRouter()

# Create LangGraph workflow
agent_graph = create_agent_graph()

# Initialize CopilotKit SDK with LangGraph agent
sdk = CopilotKitSDK(
    agents=[
        LangGraphAgent(
            name="opshub-nav-agent",
            description="AI assistant for investment operations and portfolio analysis",
            graph=agent_graph,
        ),
    ]
)

@router.websocket("/copilotkit/ws")
async def copilot_websocket(
    websocket: WebSocket,
    api_key: Optional[str] = Query(None, description="API key for authentication")
):
    """
    WebSocket endpoint for CopilotKit bidirectional communication.

    Handles:
    - Real-time state synchronization
    - Streaming agent responses
    - Tool execution events
    - API key authentication via query parameter
    """

    # Verify API key
    if not verify_api_key(api_key):
        await websocket.close(code=1008, reason="Invalid or missing API key")
        return

    await websocket.accept()
    logger.info("CopilotKit WebSocket connection accepted")

    try:
        # Let CopilotKit SDK handle the WebSocket communication
        await sdk.handle_websocket(websocket)

    except WebSocketDisconnect:
        logger.info("CopilotKit WebSocket disconnected normally")

    except Exception as e:
        logger.error(f"CopilotKit WebSocket error: {e}", exc_info=True)
        await websocket.close(code=1011, reason="Internal server error")

3. Register the Router

In your app/main.py:
from fastapi import FastAPI
from app.api.copilot import router as copilot_router

app = FastAPI()

# Include CopilotKit router
app.include_router(copilot_router)

4. Health Check Endpoint

Add a health check to verify the CopilotKit service:
@router.get("/copilotkit/health")
async def copilot_health():
    """Health check for CopilotKit endpoint."""
    return {
        "status": "healthy",
        "service": "CopilotKit Runtime",
        "agents": [
            {"name": agent.name, "description": agent.description}
            for agent in sdk.agents
        ],
        "features": [
            "WebSocket bidirectional communication",
            "LangGraph agent integration",
            "Real-time state synchronization",
            "Tool execution streaming"
        ]
    }

Frontend Setup

1. Install Dependencies

The CopilotKit packages are already included in OpsHub’s package.json:
{
  "@copilotkit/react-core": "^1.10.6",
  "@copilotkit/react-ui": "^1.10.6",
  "@copilotkit/runtime-client-gql": "^1.10.6"
}

2. Create API Proxy Route

Create app/api/copilotkit/route.ts to proxy WebSocket connections:
  • Next.js API Route
  • WebSocket Upgrade
import { buildAgentUrl } from '@/lib/config/agent-backend-url';
import { getAuthHeaders } from '@/lib/api/backend-auth';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  // Get authentication headers with JWT token
  const authHeaders = await getAuthHeaders();

  // Build backend URL
  const backendUrl = buildAgentUrl('/copilotkit/ws');

  // Proxy request to Python backend
  const response = await fetch(backendUrl, {
    method: 'POST',
    headers: {
      ...authHeaders,
      'Content-Type': 'application/json',
    },
    body: await request.text(),
  });

  return response;
}

3. Add CopilotKit Provider

Wrap your page with the CopilotKit provider:
// app/(app)/your-page/page.tsx
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";

export default function YourPage() {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <YourComponent />

      {/* Add floating chat popup */}
      <CopilotPopup
        labels={{
          title: "Portfolio Assistant",
          initial: "Hi! How can I help with your portfolio today?",
        }}
      />
    </CopilotKit>
  );
}

4. Use CopilotKit Hooks

Access chat functionality in your components:
import { useCopilotChat } from "@copilotkit/react-core";

export function ChatComponent() {
  const {
    messages,
    sendMessage,
    isLoading,
    error,
  } = useCopilotChat();

  const handleSend = async (text: string) => {
    await sendMessage({
      role: "user",
      content: text,
    });
  };

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i} className={msg.role}>
          {msg.content}
        </div>
      ))}

      {isLoading && <div>Agent is thinking...</div>}
      {error && <div>Error: {error.message}</div>}

      <input
        onKeyPress={(e) => {
          if (e.key === 'Enter') {
            handleSend(e.currentTarget.value);
          }
        }}
        placeholder="Ask me anything..."
      />
    </div>
  );
}

Complete Example

Here’s a full working example of a contextual help sidebar:
// app/(app)/portfolio/page.tsx
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import { PortfolioTable } from "@/components/portfolio/table";

export default function PortfolioPage() {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <div className="flex h-screen">
        {/* Main content */}
        <div className="flex-1">
          <h1>Portfolio Holdings</h1>
          <PortfolioTable />
        </div>

        {/* Contextual sidebar */}
        <CopilotSidebar
          labels={{
            title: "Portfolio Assistant",
            initial: "I can help you analyze holdings, run validations, or answer questions about your portfolio.",
          }}
          defaultOpen={false}
        />
      </div>
    </CopilotKit>
  );
}

Authentication

CopilotKit uses JWT tokens for secure authentication:

Server-Side (Next.js API Route)

import { getAuthHeaders } from '@/lib/api/backend-auth';

export async function POST(request: Request) {
  // Get JWT token from Supabase session
  const headers = await getAuthHeaders();
  // headers = { 'Authorization': 'Bearer <jwt_token>' }

  const response = await fetch(backendUrl, {
    headers,
    // ... rest of config
  });

  return response;
}

Backend (Python FastAPI)

from fastapi import WebSocket, Query, HTTPException
from typing import Optional

async def verify_api_key(api_key: Optional[str]) -> bool:
    """Verify JWT token or API key."""
    if not api_key:
        return False

    # Validate JWT token with Supabase
    # Or check against configured API key
    return api_key == settings.AGENT_API_KEY

@router.websocket("/copilotkit/ws")
async def copilot_websocket(
    websocket: WebSocket,
    api_key: Optional[str] = Query(None)
):
    if not await verify_api_key(api_key):
        await websocket.close(code=1008, reason="Invalid API key")
        return

    await websocket.accept()
    await sdk.handle_websocket(websocket)
Always use JWT tokens from Supabase sessions instead of user IDs. The getAuthHeaders() utility automatically extracts the JWT token from the session.

Error Handling

Connection Failures

Handle WebSocket connection failures gracefully:
import { CopilotKit } from "@copilotkit/react-core";
import { useState } from "react";

export function RobustCopilotProvider({ children }) {
  const [connectionError, setConnectionError] = useState<string | null>(null);

  return (
    <CopilotKit
      runtimeUrl="/api/copilotkit"
      onError={(error) => {
        console.error("CopilotKit error:", error);
        setConnectionError(error.message);
      }}
    >
      {connectionError && (
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
          Connection error: {connectionError}
        </div>
      )}
      {children}
    </CopilotKit>
  );
}

Reconnection Logic

The WebSocket connection automatically reconnects on disconnect:
import { useCopilotChat } from "@copilotkit/react-core";
import { useEffect } from "react";

export function ChatWithReconnect() {
  const { connectionState, reconnect } = useCopilotChat();

  useEffect(() => {
    if (connectionState === "disconnected") {
      // Attempt reconnection after 3 seconds
      const timer = setTimeout(() => {
        reconnect();
      }, 3000);

      return () => clearTimeout(timer);
    }
  }, [connectionState, reconnect]);

  return (
    <div>
      {connectionState === "disconnected" && (
        <div className="text-yellow-600">
          Connection lost. Reconnecting...
        </div>
      )}
      {/* Rest of chat UI */}
    </div>
  );
}

Backend Error Handling

@router.websocket("/copilotkit/ws")
async def copilot_websocket(websocket: WebSocket):
    try:
        await websocket.accept()
        await sdk.handle_websocket(websocket)

    except WebSocketDisconnect:
        logger.info("Client disconnected normally")

    except ValueError as e:
        logger.error(f"Invalid data: {e}")
        await websocket.close(code=1007, reason="Invalid data format")

    except Exception as e:
        logger.error(f"Unexpected error: {e}", exc_info=True)
        await websocket.close(code=1011, reason="Internal server error")

Best Practices

Performance

Optimize for low latency:
  • Keep messages small (avoid sending large JSON payloads)
  • Use message batching for multiple rapid updates
  • Implement debouncing for user input
  • Close connections when not in use

User Experience

Provide clear feedback:
  • Show typing indicators while agent is thinking
  • Display connection status in the UI
  • Handle errors gracefully with user-friendly messages
  • Provide retry mechanisms for failed messages

Security

Always secure your connections:
  • Use JWT tokens, never expose API keys to the frontend
  • Validate all user inputs on the backend
  • Implement rate limiting to prevent abuse
  • Log all authentication failures

State Management

Keep state simple:
  • CopilotKit handles client-side state automatically
  • Store conversation history in your database if persistence is needed
  • Use Supabase for session data (see agent.sessions table)
  • Clean up old sessions periodically

Troubleshooting

WebSocket Connection Fails

Problem: Connection immediately closes or never establishes. Solutions:
  1. Check that backend is running: curl http://localhost:8000/copilotkit/health
  2. Verify API proxy route exists: app/api/copilotkit/route.ts
  3. Check browser console for authentication errors
  4. Ensure runtimeUrl prop matches your API route
// Correct
<CopilotKit runtimeUrl="/api/copilotkit">

// Incorrect - missing /api prefix
<CopilotKit runtimeUrl="/copilotkit">

Authentication Errors

Problem: Backend returns 401 or 1008 WebSocket close code. Solutions:
  1. Verify Supabase session exists: supabase.auth.getSession()
  2. Check JWT token is being sent: inspect network request headers
  3. Ensure backend API key matches environment variable
  4. Test with the health endpoint first
# Test authentication
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  http://localhost:8000/copilotkit/health

Messages Not Streaming

Problem: Agent responses appear all at once instead of streaming. Solutions:
  1. Verify backend is using yield for streaming responses
  2. Check Content-Type header is text/event-stream
  3. Ensure proxy is not buffering responses
  4. Test directly with backend (bypass Next.js proxy)

High Latency

Problem: Slow response times or delayed message delivery. Solutions:
  1. Check network latency: test direct backend connection
  2. Optimize LangGraph workflow (reduce tool calls)
  3. Use faster LLM models (GPT-3.5 instead of GPT-4)
  4. Implement response caching for common queries
WebSocket connections can be blocked by corporate firewalls or proxies. Consider providing an HTTP fallback option for enterprise users.

Next Steps

1

Set up your backend

Follow the Backend Setup section to create your CopilotKit endpoint
2

Add the frontend provider

Wrap your page with the CopilotKit provider as shown in Frontend Setup
3

Test the connection

Use the health endpoint to verify your setup: /api/copilotkit/health
4

Build your chat UI

Start with the Complete Example and customize for your use case