| name | chatkit-integration |
| description | Integrate OpenAI ChatKit framework with custom backend and AI agents. Handles ChatKit server implementation, React component integration, context injection, and conversation persistence. |
ChatKit Integration Skill
Persona
You are a full-stack engineer integrating OpenAI ChatKit framework with a custom backend and AI agents. You understand that ChatKit provides standardized conversation UI/UX, but requires custom integration to work with domain-specific agents and context.
Questions to Ask Before Implementing
Backend Integration:
- What agent framework are you using? (OpenAI Agents SDK, LangChain, custom)
- What tools does your agent need? (RAG search, custom functions)
- What context does your agent need? (user profile, page context, conversation history)
- What database are you using? (PostgreSQL, MongoDB, Redis)
Frontend Integration:
- What frontend framework? (React, Next.js, Docusaurus)
- How is authentication handled? (OAuth, JWT, session cookies)
- What context can you extract client-side? (page URL, title, DOM content)
- Do you need custom UI features? (text selection, personalization menu)
Context Requirements:
- What user information is available? (name, email, role, preferences)
- What page context is needed? (URL, title, headings, content)
- How should context be transmitted? (headers, metadata, query params)
- Should context be included in every request or only when needed?
Persistence Requirements:
- Do conversations need to persist across sessions?
- What's the expected conversation volume? (affects database choice)
- Do you need multi-tenancy? (organization isolation)
- What's the retention policy? (how long to keep conversations)
Principles
Backend Principles
Extend ChatKit Server, Don't Replace
- Inherit from
ChatKitServer[RequestContext] - Override only
respond()method for agent execution - Let base class handle read-only operations (threads.list, items.list)
- Rationale: ChatKit handles protocol, you handle agent logic
- Inherit from
Context Injection in Prompt
- Include conversation history as string in system prompt (CarFixer pattern)
- Include user context (name, profile) in system prompt
- Include page context (current page) in system prompt
- Rationale: Agent SDK receives single prompt, history must be in prompt
User Isolation via RequestContext
- All operations scoped by
user_idinRequestContext - Store operations filter by
user_idautomatically - Never expose data across users
- Rationale: Multi-tenant safety, data privacy
- All operations scoped by
Graceful Degradation
- System starts even if database unavailable (ChatKit disabled)
- RAG search can fail without blocking ChatKit
- Log warnings but don't crash
- Rationale: Partial functionality better than no functionality
Connection Pool Warmup
- Pre-warm database connections on startup
- Avoids 7+ second first-request delay
- Test connections before use (
pool_pre_ping=True) - Rationale: Production-ready performance
Frontend Principles
Custom Fetch Interceptor
- Provide custom
fetchfunction touseChatKitconfig - Intercept all ChatKit requests
- Add authentication headers (
X-User-ID) - Add metadata (userInfo, pageContext) to request body
- Rationale: ChatKit doesn't handle auth natively, you must inject it
- Provide custom
Script Loading Detection
- Check for ChatKit custom element before rendering
- Listen for script load events
- Only render ChatKit component when script ready
- Handle script load failures gracefully
- Rationale: External script required, component fails without it
Page Context Extraction
- Extract client-side (DOM, window.location)
- Include: URL, title, path, headings, meta description
- Send with every message (in metadata)
- Rationale: Agent needs to know what user is viewing
Build-Time Configuration
- Read env vars in
docusaurus.config.ts(build-time) - Add to
customFieldsfor client-side access - Don't use
process.envin browser code - Rationale: Static sites bake config at build time
- Read env vars in
Authentication Gate
- Require login before allowing chat access
- Show login prompt if not authenticated
- Redirect to OAuth flow
- Rationale: User ID required for conversation persistence
Implementation Patterns
Pattern 1: ChatKit Server with Custom Agent
When: Integrating ChatKit with OpenAI Agents SDK
Implementation:
from chatkit.server import ChatKitServer
from agents import Agent, Runner
from chatkit.agents import stream_agent_response
class CustomChatKitServer(ChatKitServer[RequestContext]):
"""Extend ChatKit server with custom agent."""
async def respond(
self,
thread: ThreadMetadata,
input_user_message: UserMessageItem | None,
context: RequestContext,
) -> AsyncIterator[ThreadStreamEvent]:
# Only handle user messages (let base class handle read-only ops)
if not input_user_message:
return
# Load conversation history
previous_items = await self.store.load_thread_items(
thread.id, after=None, limit=10, order="desc", context=context
)
# Build history string for prompt
history_str = "\n".join([
f"{item.role}: {item.content}"
for item in reversed(previous_items.data)
])
# Extract context from metadata
user_info = context.metadata.get('userInfo', {})
page_context = context.metadata.get('pageContext', {})
# Build context strings
user_context_str = f"\nUser: {user_info.get('name')}\n"
page_context_str = f"\nPage: {page_context.get('title')}\n"
# Create agent with tools
agent = Agent(
name="Assistant",
tools=[your_search_tool],
instructions=f"{history_str}\n{user_context_str}{page_context_str}\n{system_prompt}",
)
# Convert message to agent input
converter = YourThreadItemConverter()
agent_input = await converter.to_agent_input(input_user_message)
# Run agent with streaming
agent_context = YourAgentContext(
thread=thread,
store=self.store,
request_context=context,
)
result = Runner.run_streamed(agent, agent_input, context=agent_context)
# Stream results
async for event in stream_agent_response(agent_context, result):
yield event
Evidence: rag-agent/chatkit_server.py:100-270
Pattern 2: Custom Fetch Interceptor
When: Adding authentication and context to ChatKit requests
Implementation:
const { control, sendUserMessage } = useChatKit({
api: {
url: `${backendUrl}/chatkit`,
domainKey: domainKey,
// Custom fetch to inject auth and context
fetch: async (url: string, options: RequestInit) => {
// Check authentication
if (!isLoggedIn) {
throw new Error('User must be logged in');
}
const userId = session.user.id;
const pageContext = getPageContext();
const userInfo = {
id: userId,
name: session.user.name,
email: session.user.email,
// ... other user fields
};
// Modify request body to add metadata
let modifiedOptions = { ...options };
if (modifiedOptions.body && typeof modifiedOptions.body === 'string') {
const parsed = JSON.parse(modifiedOptions.body);
if (parsed.type === 'threads.create' && parsed.params?.input) {
parsed.params.input.metadata = {
userId,
userInfo,
pageContext,
...parsed.params.input.metadata,
};
modifiedOptions.body = JSON.stringify(parsed);
} else if (parsed.type === 'threads.run' && parsed.params?.input) {
if (!parsed.params.input.metadata) {
parsed.params.input.metadata = {};
}
parsed.params.input.metadata.userInfo = userInfo;
parsed.params.input.metadata.pageContext = pageContext;
modifiedOptions.body = JSON.stringify(parsed);
}
}
// Add authentication header
return fetch(url, {
...modifiedOptions,
headers: {
...modifiedOptions.headers,
'X-User-ID': userId,
'Content-Type': 'application/json',
},
});
},
},
});
Evidence: robolearn-interface/src/components/ChatKitWidget/index.tsx:197-240
Pattern 3: Script Loading Detection
When: ChatKit requires external script, component must wait
Implementation:
const [scriptStatus, setScriptStatus] = useState<'pending' | 'ready' | 'error'>(
isBrowser && window.customElements?.get('openai-chatkit') ? 'ready' : 'pending'
);
useEffect(() => {
if (scriptStatus !== 'pending') return;
// Check if already loaded
if (window.customElements?.get('openai-chatkit')) {
setScriptStatus('ready');
return;
}
// Listen for script load events
const handleLoaded = () => setScriptStatus('ready');
const handleError = () => setScriptStatus('error');
window.addEventListener('chatkit-script-loaded', handleLoaded);
window.addEventListener('chatkit-script-error', handleError);
// Timeout after 5 seconds
const timeoutId = setTimeout(() => {
if (scriptStatus === 'pending') {
setScriptStatus('error');
}
}, 5000);
return () => {
window.removeEventListener('chatkit-script-loaded', handleLoaded);
window.removeEventListener('chatkit-script-error', handleError);
clearTimeout(timeoutId);
};
}, [scriptStatus]);
// Only render ChatKit when script ready
{isOpen && scriptStatus === 'ready' && (
<ChatKit control={control} />
)}
Evidence: robolearn-interface/src/components/ChatKitWidget/index.tsx:67-113
Pattern 4: Page Context Extraction
When: Agent needs to know what page user is viewing
Implementation:
const getPageContext = useCallback(() => {
if (typeof window === 'undefined') return null;
// Extract meta tags
const metaDescription = document.querySelector('meta[name="description"]')
?.getAttribute('content') || '';
// Find main content
const mainContent = document.querySelector('article') ||
document.querySelector('main') ||
document.body;
// Extract headings
const headings = Array.from(mainContent.querySelectorAll('h1, h2, h3'))
.slice(0, 5)
.map(h => h.textContent?.trim())
.filter(Boolean)
.join(', ');
return {
url: window.location.href,
title: document.title,
path: window.location.pathname,
description: metaDescription,
headings: headings,
timestamp: new Date().toISOString(),
};
}, []);
Evidence: robolearn-interface/src/components/ChatKitWidget/index.tsx:121-151
Pattern 5: Text Selection "Ask" Feature
When: Users want to ask questions about selected content
Implementation:
// Detect text selection
useEffect(() => {
const handleSelection = () => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
setSelectedText('');
setSelectionPosition(null);
return;
}
const selectedText = selection.toString().trim();
if (selectedText.length > 0) {
setSelectedText(selectedText);
// Get selection position
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
setSelectionPosition({
x: rect.left + rect.width / 2,
y: rect.top - 10,
});
}
};
document.addEventListener('selectionchange', handleSelection);
document.addEventListener('mouseup', handleSelection);
return () => {
document.removeEventListener('selectionchange', handleSelection);
document.removeEventListener('mouseup', handleSelection);
};
}, []);
// Send selected text
const handleAskSelectedText = useCallback(async () => {
const pageContext = getPageContext();
const messageText = `Can you explain this from "${pageContext.title}":\n\n"${selectedText}"`;
if (!isOpen) {
setIsOpen(true);
await new Promise(resolve => setTimeout(resolve, 300));
}
await sendUserMessage({
text: messageText,
newThread: false,
});
// Clear selection
window.getSelection()?.removeAllRanges();
setSelectedText('');
setSelectionPosition(null);
}, [selectedText, isOpen, sendUserMessage, getPageContext]);
Evidence: robolearn-interface/src/components/ChatKitWidget/index.tsx:153-187, 273-331
When to Apply
- Integrating ChatKit with custom backend
- Adding authentication to ChatKit
- Injecting context (user, page) into agent prompts
- Implementing text selection "Ask" functionality
- Building conversational AI interfaces
Contraindications
- Simple chat without persistence: ChatKit may be overkill
- No user authentication: ChatKit requires user_id for isolation
- Serverless functions: Connection pooling doesn't work well
- Very low traffic: Overhead not justified
Common Pitfalls
History Not in Prompt: Agent doesn't remember conversation
- Fix: Include history as string in system prompt (not as messages)
Context Not Transmitted: Agent doesn't receive user/page context
- Fix: Add to request metadata, extract in backend, include in prompt
Script Not Loaded: ChatKit component fails to render
- Fix: Detect script loading, wait before rendering
Auth Headers Missing: Backend rejects requests
- Fix: Use custom fetch interceptor to add headers
Database Not Warmed: First request takes 7+ seconds
- Fix: Pre-warm connection pool on startup
References
- ChatKit Server Spec:
specs/007-chatkit-server/spec.md - ChatKit UI Spec:
specs/008-chatkit-ui-widget/spec.md - Implementation:
rag-agent/chatkit_server.py,robolearn-interface/src/components/ChatKitWidget/