$npx -y skills add mjunaidca/mjs-agent-skills --skill building-chat-interfacesBuild AI chat interfaces with custom backends, authentication, and context injection. Use when integrating chat UI with AI agents, adding auth to chat, injecting user/page context, or implementing httpOnly cookie proxies. Covers ChatKitServer, useChatKit, and MCP auth patterns. N
| 1 | # Building Chat Interfaces |
| 2 | |
| 3 | Build production-grade AI chat interfaces with custom backend integration. |
| 4 | |
| 5 | ## Quick Start |
| 6 | |
| 7 | ```bash |
| 8 | # Backend (Python) |
| 9 | uv add chatkit-sdk agents httpx |
| 10 | |
| 11 | # Frontend (React) |
| 12 | npm install @openai/chatkit-react |
| 13 | ``` |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## Core Architecture |
| 18 | |
| 19 | ``` |
| 20 | Frontend (React) Backend (Python) |
| 21 | ┌─────────────────┐ ┌─────────────────┐ |
| 22 | │ useChatKit() │───HTTP/SSE───>│ ChatKitServer │ |
| 23 | │ - custom fetch │ │ - respond() │ |
| 24 | │ - auth headers │ │ - store │ |
| 25 | │ - page context │ │ - agent │ |
| 26 | └─────────────────┘ └─────────────────┘ |
| 27 | ``` |
| 28 | |
| 29 | --- |
| 30 | |
| 31 | ## Backend Patterns |
| 32 | |
| 33 | ### 1. ChatKit Server with Custom Agent |
| 34 | |
| 35 | ```python |
| 36 | from chatkit.server import ChatKitServer |
| 37 | from chatkit.agents import stream_agent_response |
| 38 | from agents import Agent, Runner |
| 39 | |
| 40 | class CustomChatKitServer(ChatKitServer[RequestContext]): |
| 41 | """Extend ChatKit server with custom agent.""" |
| 42 | |
| 43 | async def respond( |
| 44 | self, |
| 45 | thread: ThreadMetadata, |
| 46 | input_user_message: UserMessageItem | None, |
| 47 | context: RequestContext, |
| 48 | ) -> AsyncIterator[ThreadStreamEvent]: |
| 49 | if not input_user_message: |
| 50 | return |
| 51 | |
| 52 | # Load conversation history |
| 53 | previous_items = await self.store.load_thread_items( |
| 54 | thread.id, after=None, limit=10, order="desc", context=context |
| 55 | ) |
| 56 | |
| 57 | # Build history string for prompt |
| 58 | history_str = "\n".join([ |
| 59 | f"{item.role}: {item.content}" |
| 60 | for item in reversed(previous_items.data) |
| 61 | ]) |
| 62 | |
| 63 | # Extract context from metadata |
| 64 | user_info = context.metadata.get('userInfo', {}) |
| 65 | page_context = context.metadata.get('pageContext', {}) |
| 66 | |
| 67 | # Create agent with context in instructions |
| 68 | agent = Agent( |
| 69 | name="Assistant", |
| 70 | tools=[your_search_tool], |
| 71 | instructions=f"{history_str}\nUser: {user_info.get('name')}\n{system_prompt}", |
| 72 | ) |
| 73 | |
| 74 | # Run agent with streaming |
| 75 | result = Runner.run_streamed(agent, input_user_message.content) |
| 76 | async for event in stream_agent_response(context, result): |
| 77 | yield event |
| 78 | ``` |
| 79 | |
| 80 | ### 2. Database Persistence |
| 81 | |
| 82 | ```python |
| 83 | from sqlmodel.ext.asyncio.session import AsyncSession |
| 84 | from sqlalchemy.ext.asyncio import create_async_engine |
| 85 | |
| 86 | DATABASE_URL = os.getenv("DATABASE_URL").replace("postgresql://", "postgresql+asyncpg://") |
| 87 | engine = create_async_engine(DATABASE_URL, pool_pre_ping=True) |
| 88 | |
| 89 | # Pre-warm connections on startup |
| 90 | async def warmup_pool(): |
| 91 | async with engine.begin() as conn: |
| 92 | await conn.execute(text("SELECT 1")) |
| 93 | ``` |
| 94 | |
| 95 | ### 3. JWT/JWKS Authentication |
| 96 | |
| 97 | ```python |
| 98 | from jose import jwt |
| 99 | import httpx |
| 100 | |
| 101 | async def get_current_user(authorization: str = Header()): |
| 102 | token = authorization.replace("Bearer ", "") |
| 103 | async with httpx.AsyncClient() as client: |
| 104 | jwks = (await client.get(JWKS_URL)).json() |
| 105 | payload = jwt.decode(token, jwks, algorithms=["RS256"]) |
| 106 | return payload |
| 107 | ``` |
| 108 | |
| 109 | --- |
| 110 | |
| 111 | ## Frontend Patterns |
| 112 | |
| 113 | ### 1. Custom Fetch Interceptor |
| 114 | |
| 115 | ```typescript |
| 116 | const { control, sendUserMessage } = useChatKit({ |
| 117 | api: { |
| 118 | url: `${backendUrl}/chatkit`, |
| 119 | domainKey: domainKey, |
| 120 | |
| 121 | // Custom fetch to inject auth and context |
| 122 | fetch: async (url: string, options: RequestInit) => { |
| 123 | if (!isLoggedIn) { |
| 124 | throw new Error('User must be logged in'); |
| 125 | } |
| 126 | |
| 127 | const pageContext = getPageContext(); |
| 128 | const userInfo = { id: userId, name: user.name }; |
| 129 | |
| 130 | // Inject metadata into request body |
| 131 | let modifiedOptions = { ...options }; |
| 132 | if (modifiedOptions.body && typeof modifiedOptions.body === 'string') { |
| 133 | const parsed = JSON.parse(modifiedOptions.body); |
| 134 | if (parsed.params?.input) { |
| 135 | parsed.params.input.metadata = { |
| 136 | userId, userInfo, pageContext, |
| 137 | ...parsed.params.input.metadata, |
| 138 | }; |
| 139 | modifiedOptions.body = JSON.stringify(parsed); |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | return fetch(url, { |
| 144 | ...modifiedOptions, |
| 145 | headers: { |
| 146 | ...modifiedOptions.headers, |
| 147 | 'X-User-ID': userId, |
| 148 | 'Content-Type': 'application/json', |
| 149 | }, |
| 150 | }); |
| 151 | }, |
| 152 | }, |
| 153 | }); |
| 154 | ``` |
| 155 | |
| 156 | ### 2. Page Context Extraction |
| 157 | |
| 158 | ```typescript |
| 159 | const getPageContext = useCallback(() => { |
| 160 | if (typeof window === 'undefined') return null; |
| 161 | |
| 162 | const metaDescription = document.querySelector('meta[name="description"]') |
| 163 | ?.getAttribute('content') || ''; |
| 164 | |
| 165 | const mainContent = document.querySelector('article') || |
| 166 | document.querySelector('main') || |