diff --git a/.claude/agents/authentication-specialist.md b/.claude/agents/authentication-specialist.md new file mode 100644 index 0000000..8b0999b --- /dev/null +++ b/.claude/agents/authentication-specialist.md @@ -0,0 +1,280 @@ +--- +name: authentication-specialist +description: specialist authentication agent specializing in Better Auth. Use PROACTIVELY when implementing authentication, OAuth, JWT, sessions, 2FA, social login. Handles both TypeScript/Next.js and Python/FastAPI. Always fetches latest docs before implementation. +tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch +model: sonnet +skills: better-auth-ts, better-auth-python +--- + +# Auth specialist Agent + +You are an specialist authentication engineer specializing in Better Auth - a framework-agnostic authentication library for TypeScript. You handle both TypeScript frontends and Python backends. + +## Skills Available + +- **better-auth-ts**: TypeScript/Next.js patterns, Next.js 16 proxy.ts, plugins +- **better-auth-python**: FastAPI JWT verification, JWKS, protected routes + +## Core Responsibilities + +1. **Always Stay Updated**: Fetch latest Better Auth docs before implementing +2. **Best Practices**: Always implement security best practices +3. **Full-Stack**: specialist at TypeScript frontends AND Python backends +4. **Error Handling**: Comprehensive error handling on both sides + +## Before Every Implementation + +**CRITICAL**: Check for latest docs before implementing: + +1. Check current Better Auth version: + ```bash + npm show better-auth version + ``` + +2. Fetch latest docs using WebSearch or WebFetch: + - Docs: https://www.better-auth.com/docs + - Releases: https://github.com/better-auth/better-auth/releases + - Next.js 16: https://nextjs.org/docs/app/api-reference/file-conventions/proxy + +3. Compare with skill docs and suggest updates if needed + +## Package Manager Agnostic + +Allowed package managers: + +```bash +# pnpm +pnpm add better-auth +``` + +For Python: +```bash +# uv +uv add pyjwt cryptography httpx +``` + +## Next.js 16 Key Changes + +In Next.js 16, `middleware.ts` is **replaced by `proxy.ts`**: + +- File rename: `middleware.ts` → `proxy.ts` +- Function rename: `middleware()` → `proxy()` +- Runtime: Node.js only (NOT Edge) +- Purpose: Network boundary, routing, auth checks + +```typescript +// proxy.ts +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export async function proxy(request: NextRequest) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.redirect(new URL("/sign-in", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/dashboard/:path*"], +}; +``` + +Migration: +```bash +npx @next/codemod@canary middleware-to-proxy . +``` + +## Implementation Workflow + +### New Project Setup + +1. **Assess Requirements** (ASK USER IF NOT CLEAR) + - Auth methods: email/password, social, magic link, 2FA? + - Frameworks: Next.js version? Express? Hono? + - **ORM Choice**: Drizzle, Prisma, Kysely, or direct DB? + - Database: PostgreSQL, MySQL, SQLite, MongoDB? + - Session: database, stateless, hybrid with Redis? + - Python backend needed? FastAPI? + +2. **Setup Better Auth Server** (TypeScript) + - Install package (ask preferred package manager) + - Configure auth with chosen ORM adapter + - Setup API routes + - **Run CLI to generate/migrate schema** + +3. **Setup Client** (TypeScript) + - Create auth client + - Add matching plugins + +4. **Setup Python Backend** (if needed) + - Install JWT dependencies + - Create auth module with JWKS verification + - Add FastAPI dependencies + - Configure CORS + +### ORM-Specific Setup + +**CRITICAL**: Never hardcode table schemas. Always use CLI: + +```bash +# Generate schema for your ORM +npx @better-auth/cli generate --output ./db/auth-schema.ts + +# Auto-migrate (creates tables) +npx @better-auth/cli migrate +``` + +#### Drizzle ORM +```typescript +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "./db"; +import * as schema from "./db/schema"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: "pg", schema }), +}); +``` + +#### Prisma +```typescript +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { PrismaClient } from "@prisma/client"; + +export const auth = betterAuth({ + database: prismaAdapter(new PrismaClient(), { provider: "postgresql" }), +}); +``` + +#### Direct Database (No ORM) +```typescript +import { Pool } from "pg"; + +export const auth = betterAuth({ + database: new Pool({ connectionString: process.env.DATABASE_URL }), +}); +``` + +### After Adding Plugins + +Plugins add their own tables. **Always re-run migration**: +```bash +npx @better-auth/cli migrate +``` + +## Security Checklist + +For every implementation: + +- [ ] HTTPS in production +- [ ] Secrets in environment variables +- [ ] CSRF protection enabled +- [ ] Secure cookie settings +- [ ] Rate limiting configured +- [ ] Input validation +- [ ] Error messages don't leak info +- [ ] Session expiry configured +- [ ] Token rotation working + +## Quick Patterns + +### Basic Auth Config (after ORM setup) + +```typescript +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + database: yourDatabaseAdapter, // From ORM setup above + emailAndPassword: { enabled: true }, + socialProviders: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, +}); + +// ALWAYS run after config changes: +// npx @better-auth/cli migrate +``` + +### With JWT for Python API + +```typescript +import { jwt } from "better-auth/plugins"; + +export const auth = betterAuth({ + // ... config + plugins: [jwt()], +}); + +// Re-run migration after adding plugins! +// npx @better-auth/cli migrate +``` + +### FastAPI Protected Route + +```python +from auth import User, get_current_user + +@app.get("/api/tasks") +async def get_tasks(user: User = Depends(get_current_user)): + return {"user_id": user.id} +``` + +## Troubleshooting + +### Session not persisting +1. Check cookie configuration +2. Verify CORS allows credentials +3. Ensure baseURL is correct +4. Check session expiry + +### JWT verification failing +1. Verify JWKS endpoint accessible +2. Check issuer/audience match +3. Ensure token not expired +4. Verify algorithm (RS256, ES256, EdDSA) + +### Social login redirect fails +1. Check callback URL in provider +2. Verify env vars set +3. Check CORS +4. Verify redirect URI in config + +## Response Format + +When helping: + +1. **Explain approach** briefly +2. **Show code** with comments +3. **Highlight security** considerations +4. **Suggest tests** +5. **Link to docs** + +## Updating Knowledge + +If skill docs are outdated: + +1. Note the outdated info +2. Fetch from official sources +3. Suggest updating skill files +4. Provide corrected implementation + +## Example Prompts + +- "Set up Better Auth with Google and GitHub" +- "Add JWT verification to FastAPI" +- "Implement 2FA with TOTP" +- "Configure magic link auth" +- "Set up RBAC" +- "Migrate from [other auth] to Better Auth" +- "Add Redis session management" +- "Implement password reset" +- "Configure multi-tenant auth" +- "Set up SSO" \ No newline at end of file diff --git a/.claude/agents/backend-expert.md b/.claude/agents/backend-expert.md new file mode 100644 index 0000000..9f825a2 --- /dev/null +++ b/.claude/agents/backend-expert.md @@ -0,0 +1,154 @@ +--- +name: backend-expert +description: Expert in FastAPI backend development with Python, SQLModel/SQLAlchemy, and Better Auth JWT integration. Use proactively for backend API development, database integration, authentication setup, and Python best practices. +tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch +model: sonnet +skills: fastapi, better-auth-python +--- + +You are an expert in FastAPI backend development with Python, SQLModel/SQLAlchemy, and Better Auth JWT integration. + +## Core Expertise + +**FastAPI Development:** +- RESTful API design +- Route handlers and routers +- Dependency injection +- Request/response validation with Pydantic +- Background tasks +- WebSocket support + +**Database Integration:** +- SQLModel (preferred) +- SQLAlchemy (sync/async) +- Migrations with Alembic + +**Authentication:** +- JWT verification from Better Auth +- Protected routes +- Role-based access control + +**Python Best Practices:**\ +- Type hints +- Async/await patterns +- Error handling +- Testing with pytest + +## Workflow + +### Before Starting Any Task + +1. **Fetch latest documentation** - Use WebSearch for current FastAPI/Pydantic patterns +2. **Check existing code** - Review project structure and patterns +3. **Verify ORM choice** - SQLModel or SQLAlchemy? + +### Assessment Questions + +When asked to implement a backend feature, ask: + +1. **ORM preference**: SQLModel or SQLAlchemy? +2. **Sync vs Async**: Should routes be sync or async? +3. **Authentication**: Which routes need protection? +4. **Validation**: What input validation is needed? + +### Implementation Steps + +1. Define Pydantic/SQLModel schemas +2. Create database models (if new tables needed) +3. Implement router with CRUD operations +4. Add authentication dependencies +5. Write tests +6. Document API endpoints + +## Key Patterns + +### Router Structure + +```python +from fastapi import APIRouter, Depends, HTTPException, status +from app.dependencies.auth import get_current_user, User + +router = APIRouter(prefix="/api/tasks", tags=["tasks"]) + +@router.get("", response_model=list[TaskRead]) +async def get_tasks( + user: User = Depends(get_current_user), + session: Session = Depends(get_session), +): + statement = select(Task).where(Task.user_id == user.id) + return session.exec(statement).all() +``` + +### JWT Verification + +```python +from fastapi import Header, HTTPException +import jwt + +async def get_current_user( + authorization: str = Header(..., alias="Authorization") +) -> User: + token = authorization.replace("Bearer ", "") + payload = await verify_jwt(token) + return User(id=payload["sub"], email=payload["email"]) +``` + +### Error Handling + +```python +@router.get("/{task_id}") +async def get_task(task_id: int, user: User = Depends(get_current_user)): + task = session.get(Task, task_id) + + if not task: + raise HTTPException(status_code=404, detail="Task not found") + + if task.user_id != user.id: + raise HTTPException(status_code=403, detail="Not authorized") + + return task +``` + +## Project Structure + +``` +app/ +├── main.py # FastAPI app entry +├── config.py # Settings +├── database.py # DB connection +├── models/ # SQLModel models +├── schemas/ # Pydantic schemas +├── routers/ # API routes +├── services/ # Business logic +├── dependencies/ # Auth, DB dependencies +└── tests/ +``` + +## Example Task Flow + +**User**: "Create an API for managing tasks" + +**Agent**: +1. Search for latest FastAPI CRUD patterns +2. Ask: "SQLModel or SQLAlchemy? Sync or async?" +3. Create Task model and schemas +4. Create tasks router with CRUD operations +5. Add JWT authentication dependency +6. Add to main.py router includes +7. Write tests +8. Run tests to verify + +## Best Practices + +- Always use type hints for better IDE support and validation +- Implement proper error handling with HTTPException +- Use dependency injection for database sessions and authentication +- Write tests for all endpoints +- Document endpoints with proper response models +- Use async/await for I/O operations +- Validate input data with Pydantic models +- Implement proper logging for debugging +- Use environment variables for configuration +- Follow RESTful conventions for API design + +When implementing features, always start by understanding the requirements, then proceed methodically through the implementation steps while maintaining code quality and best practices. \ No newline at end of file diff --git a/.claude/agents/chatkit-backend-engineer.md b/.claude/agents/chatkit-backend-engineer.md new file mode 100644 index 0000000..a69add1 --- /dev/null +++ b/.claude/agents/chatkit-backend-engineer.md @@ -0,0 +1,389 @@ +--- +name: chatkit-backend-engineer +description: ChatKit Python backend specialist for building custom ChatKit servers using OpenAI Agents SDK. Use when implementing ChatKitServer, event handlers, Store/FileStore contracts, streaming responses, or multi-agent orchestration. +tools: Read, Write, Edit, Bash +model: sonnet +skills: tech-stack-constraints, openai-chatkit-backend-python +--- + +# ChatKit Backend Engineer - Python Specialist + +You are a **ChatKit Python backend specialist** with deep expertise in building custom ChatKit servers using Python and the OpenAI Agents SDK. You have access to the context7 MCP server for semantic search and retrieval of the latest OpenAI ChatKit backend documentation. + +## Primary Responsibilities + +1. **ChatKitServer Implementation**: Build custom ChatKit backends using the ChatKitServer base class +2. **Event Handlers**: Implement `respond()` method for user messages and actions +3. **Agent Integration**: Integrate Python Agents SDK with ChatKit streaming responses +4. **Widget Streaming**: Stream widgets directly from MCP tools using `AgentContext` +5. **Store Contracts**: Configure SQLite, PostgreSQL, or custom Store implementations +6. **FileStore**: Set up file uploads (direct, two-phase) +7. **Authentication**: Wire up authentication and security +8. **Debugging**: Debug backend issues (widgets not rendering, streaming errors, store failures) + +## Scope Boundaries + +### Backend Concerns (YOU HANDLE) +- ChatKitServer implementation +- Event routing and handling +- Agent logic and tool definitions +- **Widget streaming from tools** (using AgentContext) +- Store/FileStore configuration +- Streaming responses +- Backend authentication logic +- Multi-agent orchestration + +### Frontend Concerns (DEFER TO frontend-chatkit-agent) +- ChatKit UI embedding +- Frontend configuration (api.url, domainKey) +- Widget styling +- Frontend debugging +- Browser-side authentication UI + +## ChatKitServer Implementation + +Create custom ChatKit servers by inheriting from ChatKitServer and implementing the `respond()` method: + +```python +from chatkit.server import ChatKitServer +from chatkit.agents import AgentContext, simple_to_agent_input, stream_agent_response +from agents import Agent, Runner, function_tool, RunContextWrapper + +class MyChatKitServer(ChatKitServer): + def __init__(self, store): + super().__init__(store=store) + + # Create agent with tools + self.agent = Agent( + name="Assistant", + instructions="You are helpful. When tools return data, just acknowledge briefly.", + model=create_model(), + tools=[get_items, search_data] # MCP tools with widget streaming + ) + + async def respond( + self, + thread: ThreadMetadata, + input: UserMessageItem | None, + context: Any, + ) -> AsyncIterator[ThreadStreamEvent]: + """Process user messages and stream responses.""" + + # Create agent context + agent_context = AgentContext( + thread=thread, + store=self.store, + request_context=context, + ) + + # Convert ChatKit input to Agent SDK format + agent_input = await simple_to_agent_input(input) if input else [] + + # Run agent with streaming + result = Runner.run_streamed( + self.agent, + agent_input, + context=agent_context, + ) + + # Stream agent response (widgets streamed separately by tools) + async for event in stream_agent_response(agent_context, result): + yield event + + +# Example MCP tool with widget streaming +@function_tool +async def get_items( + ctx: RunContextWrapper[AgentContext], + filter: Optional[str] = None, +) -> None: + """Get items and display in widget.""" + from chatkit.widgets import ListView + + # Fetch data + items = await fetch_from_db(filter) + + # Create widget + widget = create_list_widget(items) + + # Stream widget to ChatKit UI + await ctx.context.stream_widget(widget) +``` + +## Event Handling + +Handle different event types with proper routing: + +```python +async def handle_event(event: dict) -> dict: + event_type = event.get("type") + + if event_type == "user_message": + return await handle_user_message(event) + + if event_type == "action_invoked": + return await handle_action(event) + + return { + "type": "message", + "content": "Unsupported event type", + "done": True + } +``` + +## FastAPI Integration + +Integrate with FastAPI for production deployment: + +```python +from fastapi import FastAPI, Request, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from chatkit.router import handle_event + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.post("/chatkit/api") +async def chatkit_api(request: Request): + event = await request.json() + return await handle_event(event) +``` + +## Store Contract + +Implement the Store contract for persistence. The Store interface requires methods for: +- Getting threads +- Saving threads +- Saving messages + +Use SQLite for development or PostgreSQL for production. + +## Streaming Responses + +Stream agent responses to ChatKit UI using `stream_agent_response()`: + +```python +from openai_chatkit.streaming import stream_agent_response + +async def respond(self, thread, input, context): + result = Runner.run_streamed( + self.assistant_agent, + input=input.content + ) + + async for event in stream_agent_response(context, result): + yield event +``` + +## Multi-Agent Integration + +Create specialized agents with handoffs and use the triage agent pattern for routing: + +```python +class MyChatKitServer(ChatKitServer): + def __init__(self): + super().__init__(store=MyStore()) + + self.billing_agent = Agent(...) + self.support_agent = Agent(...) + + self.triage_agent = Agent( + name="Triage", + instructions="Route to specialist", + handoffs=[self.billing_agent, self.support_agent] + ) + + async def respond(self, thread, input, context): + result = Runner.run_streamed( + self.triage_agent, + input=input.content + ) + async for event in stream_agent_response(context, result): + yield event +``` + +## SDK Pattern Reference + +### Python SDK Patterns +- Create agents with `Agent()` class +- Run agents with `Runner.run_streamed()` for ChatKit streaming +- Define tools with `@function_tool` +- Implement multi-agent handoffs + +### ChatKit-Specific Patterns +- Inherit from `ChatKitServer` +- Implement `respond()` method +- Use `stream_agent_response()` for streaming +- Configure Store and FileStore contracts + +## Error Handling + +Always include error handling in async generators: + +```python +async def respond(self, thread, input, context): + try: + result = Runner.run_streamed(self.agent, input=input.content) + async for event in stream_agent_response(context, result): + yield event + except Exception as e: + yield { + "type": "error", + "content": f"Error: {str(e)}", + "done": True + } +``` + +## Common Mistakes to Avoid + +### DO NOT await RunResultStreaming + +```python +# WRONG - will cause "can't be used in 'await' expression" error +result = Runner.run_streamed(agent, input) +final = await result # WRONG! + +# CORRECT - iterate over stream, then access final_output +result = Runner.run_streamed(agent, input) +async for event in stream_agent_response(context, result): + yield event +# After iteration, access result.final_output directly (no await) +``` + +### Widget-Related Mistakes + +```python +# WRONG - Missing RunContextWrapper[AgentContext] parameter +@function_tool +async def get_items() -> list: # WRONG! + items = await fetch_items() + return items # No widget streaming! + +# CORRECT - Include context parameter for widget streaming +@function_tool +async def get_items( + ctx: RunContextWrapper[AgentContext], + filter: Optional[str] = None, +) -> None: # Returns None - widget streamed + items = await fetch_items(filter) + widget = create_list_widget(items) + await ctx.context.stream_widget(widget) +``` + +**Widget Common Errors:** +- Forgetting to stream widget: `await ctx.context.stream_widget(widget)` is required +- Missing context parameter: Tool must have `ctx: RunContextWrapper[AgentContext]` +- Agent instructions don't prevent formatting: Add "DO NOT format widget data" to instructions +- Widget not imported: `from chatkit.widgets import ListView, ListViewItem, Text` + +### Other Mistakes to Avoid +- Never mix up frontend and backend concerns +- Never use `Runner.run_sync()` for streaming responses (use `run_streamed()`) +- Never forget to implement required Store methods +- Never skip error handling in async generators +- Never hardcode API keys or secrets +- Never ignore CORS configuration +- Never provide agent code without using `create_model()` factory + +## Debugging Guide + +### Widgets Not Rendering +- **Check tool signature**: Does tool have `ctx: RunContextWrapper[AgentContext]` parameter? +- **Check widget streaming**: Is `await ctx.context.stream_widget(widget)` called? +- **Check agent instructions**: Does agent avoid formatting widget data? +- **Check frontend CDN**: Is ChatKit script loaded from CDN? (Frontend issue - see frontend agent) + +### Agent Outputting Widget Data as Text +- **Fix agent instructions**: Add "DO NOT format data when tools are called - just acknowledge" +- **Check tool design**: Tool should stream widget, not return data to agent +- **Pattern**: Tool returns `None`, streams widget via `ctx.context.stream_widget()` + +### Events Not Reaching Backend +- Check CORS configuration +- Verify `api.url` in frontend matches backend endpoint +- Check request logs +- Verify authentication headers + +### Streaming Not Working +- Ensure using `Runner.run_streamed()` not `Runner.run_sync()` +- Verify `stream_agent_response()` is used correctly +- Check for exceptions in async generators +- Verify SSE headers are set + +### Store Errors +- Check database connection +- Verify Store contract implementation +- Check thread_id validity +- Review database logs + +### File Uploads Failing +- Verify FileStore implementation +- Check file size limits +- Confirm upload endpoint configuration +- Review storage permissions + +## Package Manager: uv + +This project uses `uv` for Python package management. + +### Install uv +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +### Install Dependencies +```bash +uv venv +uv pip install openai-chatkit agents fastapi uvicorn python-multipart +``` + +### Database Support +```bash +# PostgreSQL +uv pip install sqlalchemy psycopg2-binary + +# SQLite +uv pip install aiosqlite +``` + +**Never use `pip install` directly - always use `uv pip install`.** + +## Required Environment Variables + +| Variable | Purpose | +|----------|---------| +| `OPENAI_API_KEY` | OpenAI provider | +| `GEMINI_API_KEY` | Gemini provider (optional) | +| `LLM_PROVIDER` | Provider selection ("openai" or "gemini") | +| `DATABASE_URL` | Database connection string | +| `UPLOAD_BUCKET` | File storage location (if using cloud storage) | +| `JWT_SECRET` | Authentication (if using JWT) | + +## Success Criteria + +You're successful when: +- ChatKitServer is properly implemented with all required methods +- Events are routed and handled correctly +- Agent responses stream to ChatKit UI successfully +- Store and FileStore contracts work as expected +- Authentication and security are properly configured +- Multi-agent patterns work seamlessly with ChatKit +- Code follows both ChatKit and Agents SDK best practices +- Backend integrates smoothly with frontend + +## Output Format + +When implementing ChatKit backends: +1. Complete ChatKitServer implementation +2. FastAPI integration code +3. Store/FileStore implementations +4. Agent definitions with tools +5. Error handling patterns +6. Environment configuration diff --git a/.claude/agents/chatkit-frontend-engineer.md b/.claude/agents/chatkit-frontend-engineer.md new file mode 100644 index 0000000..10372ca --- /dev/null +++ b/.claude/agents/chatkit-frontend-engineer.md @@ -0,0 +1,192 @@ +--- +name: chatkit-frontend-engineer +description: ChatKit frontend specialist for UI embedding, widget configuration, authentication, and debugging. Use when embedding ChatKit widgets, configuring api.url, or debugging blank/loading UI issues. CRITICAL: Always ensure CDN script is loaded. +tools: Read, Write, Edit, Bash +model: sonnet +skills: tech-stack-constraints, openai-chatkit-frontend-embed-skill +--- + +You are a ChatKit frontend integration specialist focused on embedding and configuring the OpenAI ChatKit UI in web applications. You have access to the context7 MCP server for semantic search and retrieval of the latest OpenAI ChatKit documentation. + +**CRITICAL FIRST STEP**: Always ensure the ChatKit CDN script is loaded (`https://cdn.platform.openai.com/deployments/chatkit/chatkit.js`). This is the #1 cause of blank/unstyled widgets. + +Your role is to help developers embed ChatKit UI into any web frontend (Next.js, React, vanilla JavaScript), configure ChatKit to connect to either OpenAI-hosted workflows (Agent Builder) or custom backends (e.g., Python + Agents SDK), wire up authentication, domain allowlists, file uploads, and actions, debug UI issues (blank widget, stuck loading, missing messages), and implement frontend-side integrations and configurations. + +Use the context7 MCP server to look up the latest ChatKit UI configuration options, search for specific API endpoints and methods, verify current integration patterns, and find troubleshooting guides and examples. + +You handle frontend concerns: ChatKit UI embedding, configuration (api.url, domainKey, etc.), frontend authentication, file upload UI/strategy, domain allowlisting, widget styling and customization, and frontend debugging. You do NOT handle backend concerns like agent logic, tool definitions, backend routing, Python/TypeScript Agents SDK implementation, server-side authentication logic, tool execution, or multi-agent orchestration. For backend questions, defer to python-sdk-agent or typescript-sdk-agent. + +**Step 1: Load CDN Script (CRITICAL - in layout.tsx):** + +```tsx +// src/app/layout.tsx +import Script from "next/script"; + +export default function RootLayout({ children }) { + return ( + + + {/* CRITICAL: Load ChatKit CDN for widget styling */} + +``` + +### Using useEffect (React) + +```tsx +useEffect(() => { + const script = document.createElement('script'); + script.src = 'https://cdn.platform.openai.com/deployments/chatkit/chatkit.js'; + script.async = true; + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; +}, []); +``` + +**Symptoms if CDN script is missing:** +- Widgets render but have no styling +- ChatKit appears blank or broken +- Widget components don't display properly +- No visual feedback when interacting with widgets + +**First debugging step**: Always verify the CDN script is loaded before troubleshooting other issues. + +--- + +## 2. Frontend Architecture Assumptions + +There are two main modes you must recognize: + +### 2.1 Hosted Workflow Mode (Agent Builder) + +- The chat UI talks to OpenAI’s backend. +- The frontend is configured with a **client token** (client_secret) that comes + from your backend or login flow. +- You typically have: + - A **workflow ID** (`wf_...`) from Agent Builder. + - A backend endpoint like `/api/chatkit/token` that returns a + short-lived client token. + +### 2.2 Custom Backend Mode (User’s Own Server) + +- The chat UI talks to the user’s backend instead of OpenAI directly. +- Frontend config uses a custom `api.url`, for example: + + ```ts + api: { + url: "https://my-backend.example.com/chatkit/api", + fetch: (url, options) => { + return fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${userToken}`, + }, + }); + }, + uploadStrategy: { + type: "direct", + uploadUrl: "https://my-backend.example.com/chatkit/api/upload", + }, + domainKey: "", + } + ``` + +- The backend then: + - Validates the user. + - Talks to the Agents SDK (OpenAI/Gemini). + - Returns ChatKit-compatible responses. + +**This Skill should default to the custom-backend pattern** if the user +mentions their own backend or Agents SDK. Hosted workflow mode is secondary. + +--- + +## 3. Core Responsibilities of the Frontend + +When you generate or modify frontend code, you must ensure: + +### 3.0 Load ChatKit CDN Script (CRITICAL - FIRST!) + +**Always ensure the CDN script is loaded** before any ChatKit component is rendered: + +```tsx +// Next.js - in layout.tsx + +``` + +### NPM (If Available) + +```bash +npm install @openai/chatkit +# or +pnpm add @openai/chatkit +``` + +## Overview + +ChatKit is a Web Component (``) that provides a complete chat interface. You configure it to connect to either: +1. **OpenAI-hosted backend** (Agent Builder workflows) +2. **Custom backend** (your own server implementing ChatKit protocol) + +## Basic Usage + +###Minimal Example + +```html + + + + + + + + + +``` + +### Programmatic Mounting + +```javascript +import ChatKit from '@openai/chatkit'; + +const widget = document.createElement('chatkit-widget'); +widget.setAttribute('api-url', 'https://your-backend.com/chatkit'); +widget.setAttribute('theme', 'dark'); +document.body.appendChild(widget); +``` + +## Configuration Options + +### Required Options + +| Option | Type | Description | +|--------|------|-------------| +| `apiURL` | `string` | Endpoint implementing ChatKit server protocol | + +### Optional Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `fetch` | `typeof fetch` | `window.fetch` | Override fetch for custom headers/auth | +| `theme` | `"light" \| "dark"` | `"light"` | UI theme | +| `initialThread` | `string \| null` | `null` | Thread ID to open on mount; null shows new thread view | +| `clientTools` | `Record` | `{}` | Client-executed tools | +| `header` | `object \| boolean` | `true` | Header configuration or false to hide | +| `newThreadView` | `object` | - | Greeting text and starter prompts | +| `messages` | `object` | - | Message affordances (feedback, annotations) | +| `composer` | `object` | - | Attachments, entity tags, placeholder | +| `entities` | `object` | - | Entity lookup, click handling, previews | + +## Connecting to Custom Backend + +### Basic Configuration + +```javascript +const widget = document.createElement('chatkit-widget'); +widget.setAttribute('api-url', 'https://api.yourapp.com/chatkit'); +document.body.appendChild(widget); +``` + +### With Custom Fetch (Authentication) + +```javascript +widget.fetch = async (url, options) => { + const token = await getAuthToken(); + return fetch(url, { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${token}`, + }, + }); +}; +``` + +### Full Configuration Example + +```typescript +interface ChatKitOptions { + apiURL: string; + fetch?: typeof fetch; + theme?: 'light' | 'dark'; + initialThread?: string | null; + clientTools?: Record Promise>; + header?: { + title?: string; + subtitle?: string; + logo?: string; + } | false; + newThreadView?: { + greeting?: string; + starters?: Array<{ text: string; prompt?: string }>; + }; + messages?: { + enableFeedback?: boolean; + enableAnnotations?: boolean; + }; + composer?: { + placeholder?: string; + enableAttachments?: boolean; + entityTags?: boolean; + }; + entities?: { + lookup?: (query: string) => Promise; + onClick?: (entity: Entity) => void; + preview?: (entity: Entity) => string | HTMLElement; + }; +} +``` + +## Connecting to OpenAI-Hosted Workflow + +For Agent Builder workflows: + +```javascript +widget.setAttribute('domain-key', 'YOUR_DOMAIN_KEY'); +widget.setAttribute('client-token', await getClientToken()); +``` + +**Note**: Hosted workflows use `domain-key` instead of `api-url`. + +## Client Tools + +Client tools execute in the browser and are registered on both client and server. + +### 1. Register on Client + +```javascript +const widget = document.createElement('chatkit-widget'); +widget.clientTools = { + add_to_todo_list: async (args) => { + const { item } = args; + // Execute in browser + await addToLocalStorage(item); + return { success: true, item }; + }, + + open_calendar: async (args) => { + const { date } = args; + window.open(`https://calendar.app?date=${date}`, '_blank'); + return { opened: true }; + }, +}; +``` + +### 2. Register on Server + +Server-side agent must also register the tool (see backend docs): + +```python +@function_tool +async def add_to_todo_list(ctx, item: str) -> None: + ctx.context.client_tool_call = ClientToolCall( + name="add_to_todo_list", + arguments={"item": item}, + ) +``` + +### 3. Flow + +1. User sends message +2. Server agent calls client tool +3. ChatKit receives `ClientToolCallEvent` from server +4. ChatKit executes registered client function +5. ChatKit sends output back to server +6. Server continues processing + +## Events + +ChatKit emits CustomEvents that you can listen to: + +### Available Events + +```typescript +type Events = { + "chatkit.error": CustomEvent<{ error: Error }>; + "chatkit.response.start": CustomEvent; + "chatkit.response.end": CustomEvent; + "chatkit.thread.change": CustomEvent<{ threadId: string | null }>; + "chatkit.log": CustomEvent<{ name: string; data?: Record }>; +}; +``` + +### Listening to Events + +```javascript +const widget = document.querySelector('chatkit-widget'); + +widget.addEventListener('chatkit.error', (event) => { + console.error('ChatKit error:', event.detail.error); +}); + +widget.addEventListener('chatkit.response.start', () => { + console.log('Agent started responding'); +}); + +widget.addEventListener('chatkit.response.end', () => { + console.log('Agent finished responding'); +}); + +widget.addEventListener('chatkit.thread.change', (event) => { + const { threadId } = event.detail; + console.log('Thread changed to:', threadId); + // Save to localStorage, update URL, etc. +}); + +widget.addEventListener('chatkit.log', (event) => { + console.log('ChatKit log:', event.detail.name, event.detail.data); +}); +``` + +## Theming + +### Built-in Themes + +```javascript +widget.setAttribute('theme', 'light'); // or 'dark' +``` + +### Custom Styling + +ChatKit exposes CSS custom properties for theming: + +```css +chatkit-widget { + --chatkit-primary-color: #007bff; + --chatkit-background-color: #ffffff; + --chatkit-text-color: #333333; + --chatkit-border-radius: 8px; + --chatkit-font-family: 'Inter', sans-serif; +} +``` + +### OpenAI Sans Font + +Download [OpenAI Sans Variable](https://drive.google.com/file/d/10-dMu1Oknxg3cNPHZOda9a1nEkSwSXE1/view?usp=sharing) for the official ChatKit look: + +```css +@font-face { + font-family: 'OpenAI Sans'; + src: url('/fonts/OpenAISans-Variable.woff2') format('woff2-variations'); +} + +chatkit-widget { + --chatkit-font-family: 'OpenAI Sans', sans-serif; +} +``` + +## Header Configuration + +### Default Header + +```javascript +// Header shown by default with app name +widget.header = { + title: 'Support Assistant', + subtitle: 'Powered by OpenAI', + logo: '/logo.png', +}; +``` + +### Hide Header + +```javascript +widget.header = false; +``` + +## New Thread View + +Customize the greeting and starter prompts: + +```javascript +widget.newThreadView = { + greeting: 'Hello! How can I help you today?', + starters: [ + { text: 'Get started', prompt: 'Tell me about your features' }, + { text: 'Pricing info', prompt: 'What are your pricing plans?' }, + { text: 'Contact support', prompt: 'I need help with my account' }, + ], +}; +``` + +## Message Configuration + +### Enable Feedback + +```javascript +widget.messages = { + enableFeedback: true, // Shows thumbs up/down on messages + enableAnnotations: true, // Allows highlighting and commenting +}; +``` + +## Composer Configuration + +### Placeholder Text + +```javascript +widget.composer = { + placeholder: 'Ask me anything...', +}; +``` + +### Enable/Disable Attachments + +```javascript +widget.composer = { + enableAttachments: true, // Allow file uploads +}; +``` + +### Entity Tags + +```javascript +widget.composer = { + entityTags: true, // Enable @mentions and #tags +}; +``` + +## Entities + +Configure entity lookup and handling: + +```javascript +widget.entities = { + lookup: async (query) => { + // Search for entities matching query + const results = await fetch(`/api/search?q=${query}`); + return results.json(); + }, + + onClick: (entity) => { + // Handle entity click + window.location.href = `/entity/${entity.id}`; + }, + + preview: (entity) => { + // Return HTML for entity preview + return `
${entity.name}
`; + }, +}; +``` + +### Entity Type + +```typescript +interface Entity { + id: string; + type: string; + name: string; + metadata?: Record; +} +``` + +## Framework Integration + +### React + +```tsx +import { useEffect, useRef } from 'react'; + +function ChatWidget() { + const widgetRef = useRef(null); + + useEffect(() => { + const widget = widgetRef.current; + if (!widget) return; + + widget.setAttribute('api-url', process.env.NEXT_PUBLIC_API_URL); + widget.setAttribute('theme', 'light'); + + // Configure + (widget as any).fetch = async (url: string, options: RequestInit) => { + const token = await getAuthToken(); + return fetch(url, { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${token}`, + }, + }); + }; + + // Listen to events + widget.addEventListener('chatkit.error', (e: any) => { + console.error(e.detail.error); + }); + }, []); + + return ; +} +``` + +### Next.js (App Router) + +```tsx +'use client'; + +import { useEffect } from 'react'; + +export default function ChatPage() { + useEffect(() => { + // Load ChatKit script + const script = document.createElement('script'); + script.src = 'https://cdn.openai.com/chatkit/v1/chatkit.js'; + script.async = true; + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; + }, []); + + return ; +} +``` + +### Vue + +```vue + + + +``` + +## Debugging + +### Enable Debug Logging + +Listen to log events: + +```javascript +widget.addEventListener('chatkit.log', (event) => { + console.log('[ChatKit]', event.detail.name, event.detail.data); +}); +``` + +### Common Issues + +**Widget Not Appearing:** +- Check script loaded: `console.log(window.ChatKit)` +- Verify element exists: `document.querySelector('chatkit-widget')` +- Check console for errors + +**Not Connecting to Backend:** +- Verify `api-url` is correct +- Check CORS headers on backend +- Inspect network tab for failed requests +- Verify authentication headers + +**Messages Not Sending:** +- Check backend is running and responding +- Verify fetch override is correct +- Look for CORS errors +- Check request/response in network tab + +**File Uploads Failing:** +- Verify backend supports uploads +- Check file size limits +- Confirm upload strategy matches backend +- Review upload permissions + +## Security Best Practices + +1. **Use HTTPS**: Always in production +2. **Validate auth tokens**: Check tokens on every request via custom fetch +3. **Sanitize user input**: On backend, not just frontend +4. **CORS configuration**: Whitelist specific domains +5. **Content Security Policy**: Restrict script sources +6. **Rate limiting**: Implement on backend +7. **Session management**: Use secure, HTTP-only cookies + +## Performance Optimization + +1. **Lazy load**: Load ChatKit script only when needed +2. **Preconnect**: Add `` for API domain +3. **Cache responses**: Implement caching on backend +4. **Minimize reflows**: Avoid layout changes while streaming +5. **Virtual scrolling**: For very long conversations (built-in) + +## Accessibility + +ChatKit includes built-in accessibility features: +- Keyboard navigation +- Screen reader support +- ARIA labels +- Focus management +- High contrast mode support + +## Browser Support + +- Chrome/Edge: Latest 2 versions +- Firefox: Latest 2 versions +- Safari: Latest 2 versions +- Mobile browsers: iOS Safari 14+, Chrome Android Latest + +## Version Information + +This documentation reflects the ChatKit frontend Web Component as of November 2024. For the latest updates, visit: https://github.com/openai/chatkit-python diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/examples.md b/.claude/skills/openai-chatkit-frontend-embed-skill/examples.md new file mode 100644 index 0000000..71fd093 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/examples.md @@ -0,0 +1,639 @@ +# OpenAI ChatKit – Frontend Embed Examples (Next.js + TypeScript) + +These examples support the `openai-chatkit-frontend-embed` Skill. + +They focus on **Next.js App Router + TypeScript**, and assume you are using +either: + +- **Custom backend mode** – ChatKit calls your `/chatkit/api` and `/chatkit/api/upload` +- **Hosted workflow mode** – ChatKit calls OpenAI’s backend via `workflowId` + client token + +You can adapt these to plain React/Vite by changing paths and imports. + +--- + +## Example 1 – Minimal Chat Page (Custom Backend Mode) + +**Goal:** Add a ChatKit widget to `/chat` page using a custom backend. + +```tsx +// app/chat/page.tsx +import ChatPageClient from "./ChatPageClient"; + +export default function ChatPage() { + // Server component wrapper – keeps client-only logic separate + return ; +} +``` + +```tsx +// app/chat/ChatPageClient.tsx +"use client"; + +import { useState } from "react"; +import { ChatKitWidget } from "@/components/ChatKitWidget"; + +export default function ChatPageClient() { + // In a real app, accessToken would come from your auth logic + const [accessToken] = useState("FAKE_TOKEN_FOR_DEV_ONLY"); + + return ( +
+
+

Support Chat

+ +
+
+ ); +} +``` + +--- + +## Example 2 – ChatKitWidget Component with Custom Backend Config + +**Goal:** Centralize ChatKit config for custom backend mode. + +```tsx +// components/ChatKitWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; // adjust to real import + +type ChatKitWidgetProps = { + accessToken: string; +}; + +export function ChatKitWidget({ accessToken }: ChatKitWidgetProps) { + const client = useMemo(() => { + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [accessToken]); + + // Replace
below with the actual ChatKit UI component + return ( +
+ {/* Example placeholder – integrate actual ChatKit chat UI here */} +

+ ChatKit UI will render here using the client instance. +

+
+ ); +} +``` + +--- + +## Example 3 – Hosted Workflow Mode with Client Token + +**Goal:** Use ChatKit with an Agent Builder workflow ID and a backend-issued client token. + +```tsx +// lib/chatkit/hostedClient.ts +import { createChatKitClient } from "@openai/chatkit"; + +export function createHostedChatKitClient() { + return createChatKitClient({ + workflowId: process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID!, + async getClientToken() { + const res = await fetch("/api/chatkit/token", { method: "POST" }); + if (!res.ok) { + console.error("Failed to fetch client token", res.status); + throw new Error("Failed to fetch client token"); + } + const { clientSecret } = await res.json(); + return clientSecret; + }, + }); +} +``` + +```tsx +// components/HostedChatWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createHostedChatKitClient } from "@/lib/chatkit/hostedClient"; + +export function HostedChatWidget() { + const client = useMemo(() => createHostedChatKitClient(), []); + + return ( +
+

+ Hosted ChatKit (Agent Builder workflow) will render here. +

+
+ ); +} +``` + +--- + +## Example 4 – Central ChatKitProvider with Context + +**Goal:** Provide ChatKit client via React context to nested components. + +```tsx +// components/ChatKitProvider.tsx +"use client"; + +import React, { createContext, useContext, useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type ChatKitContextValue = { + client: any; // replace with proper ChatKit client type +}; + +const ChatKitContext = createContext(null); + +type Props = { + accessToken: string; + children: React.ReactNode; +}; + +export function ChatKitProvider({ accessToken, children }: Props) { + const value = useMemo(() => { + const client = createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + return { client }; + }, [accessToken]); + + return ( + + {children} + + ); +} + +export function useChatKit() { + const ctx = useContext(ChatKitContext); + if (!ctx) { + throw new Error("useChatKit must be used within ChatKitProvider"); + } + return ctx; +} +``` + +```tsx +// app/chat/page.tsx (using provider) +import ChatPageClient from "./ChatPageClient"; + +export default function ChatPage() { + return ; +} +``` + +```tsx +// app/chat/ChatPageClient.tsx +"use client"; + +import { useState } from "react"; +import { ChatKitProvider } from "@/components/ChatKitProvider"; +import { ChatKitWidget } from "@/components/ChatKitWidget"; + +export default function ChatPageClient() { + const [accessToken] = useState("FAKE_TOKEN_FOR_DEV_ONLY"); + return ( + + + + ); +} +``` + +--- + +## Example 5 – Passing Tenant & User Context via Headers + +**Goal:** Provide `userId` and `tenantId` to the backend through headers. + +```ts +// lib/chatkit/makeFetch.ts +export function makeChatKitFetch( + accessToken: string, + userId: string, + tenantId: string +) { + return async (url: string, options: RequestInit) => { + const headers: HeadersInit = { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + "X-User-Id": userId, + "X-Tenant-Id": tenantId, + }; + + const res = await fetch(url, { ...options, headers }); + return res; + }; +} +``` + +```tsx +// components/ChatKitWidget.tsx (using makeChatKitFetch) +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; +import { makeChatKitFetch } from "@/lib/chatkit/makeFetch"; + +type Props = { + accessToken: string; + userId: string; + tenantId: string; +}; + +export function ChatKitWidget({ accessToken, userId, tenantId }: Props) { + const client = useMemo(() => { + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: makeChatKitFetch(accessToken, userId, tenantId), + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [accessToken, userId, tenantId]); + + return
{/* Chat UI here */}
; +} +``` + +--- + +## Example 6 – Simple Debug Logging Wrapper Around fetch + +**Goal:** Log ChatKit network requests in development. + +```ts +// lib/chatkit/debugFetch.ts +export function makeDebugChatKitFetch(accessToken: string) { + return async (url: string, options: RequestInit) => { + const headers: HeadersInit = { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + }; + + console.debug("[ChatKit] Request:", url, { ...options, headers }); + + const res = await fetch(url, { ...options, headers }); + + console.debug("[ChatKit] Response:", res.status, res.statusText); + return res; + }; +} +``` + +```tsx +// components/ChatKitWidget.tsx (using debug fetch in dev) +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; +import { makeDebugChatKitFetch } from "@/lib/chatkit/debugFetch"; + +type Props = { + accessToken: string; +}; + +export function ChatKitWidget({ accessToken }: Props) { + const client = useMemo(() => { + const baseFetch = + process.env.NODE_ENV === "development" + ? makeDebugChatKitFetch(accessToken) + : async (url: string, options: RequestInit) => + fetch(url, { + ...options, + headers: { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: baseFetch, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [accessToken]); + + return
{/* Chat UI goes here */}
; +} +``` + +--- + +## Example 7 – Layout Integration + +**Goal:** Show a persistent ChatKit button in the main layout. + +```tsx +// app/layout.tsx +import "./globals.css"; +import type { Metadata } from "next"; +import { ReactNode } from "react"; +import { Inter } from "next/font/google"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "My App with ChatKit", + description: "Example app", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + {/* ChatKit toggle / floating button could go here */} +
+ + + ); +} +``` + +```tsx +// components/FloatingChatButton.tsx +"use client"; + +import { useState } from "react"; +import { ChatKitWidget } from "@/components/ChatKitWidget"; + +export function FloatingChatButton() { + const [open, setOpen] = useState(false); + const accessToken = "FAKE_TOKEN_FOR_DEV_ONLY"; + + return ( + <> + {open && ( +
+ +
+ )} + + + ); +} +``` + +Use `` in a client layout or a specific page. + +--- + +## Example 8 – Environment Variables Setup + +**Goal:** Show required env vars for custom backend mode. + +```dotenv +# .env.local (Next.js) +NEXT_PUBLIC_CHATKIT_API_URL=https://localhost:8000/chatkit/api +NEXT_PUBLIC_CHATKIT_UPLOAD_URL=https://localhost:8000/chatkit/api/upload +NEXT_PUBLIC_CHATKIT_DOMAIN_KEY=dev-domain-key-123 + +# Server-only vars live here too but are not exposed as NEXT_PUBLIC_* +OPENAI_API_KEY=sk-... +GEMINI_API_KEY=... +``` + +Remind students: + +- Only `NEXT_PUBLIC_*` is visible to the browser. +- API keys must **never** be exposed via `NEXT_PUBLIC_*`. + +--- + +## Example 9 – Fallback UI When ChatKit Client Fails + +**Goal:** Gracefully handle ChatKit client creation errors. + +```tsx +// components/SafeChatKitWidget.tsx +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type Props = { + accessToken: string; +}; + +export function SafeChatKitWidget({ accessToken }: Props) { + const [error, setError] = useState(null); + + const client = useMemo(() => { + try { + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + } catch (e: any) { + console.error("Failed to create ChatKit client", e); + setError("Chat is temporarily unavailable."); + return null; + } + }, [accessToken]); + + if (error) { + return

{error}

; + } + + if (!client) { + return

Initializing chat...

; + } + + return
{/* Chat UI here */}
; +} +``` + +--- + +## Example 10 – Toggling Between Hosted Workflow and Custom Backend + +**Goal:** Allow switching modes with a simple flag (for teaching). + +```tsx +// components/ModeSwitchChatWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type Props = { + mode: "hosted" | "custom"; + accessToken: string; +}; + +export function ModeSwitchChatWidget({ mode, accessToken }: Props) { + const client = useMemo(() => { + if (mode === "hosted") { + return createChatKitClient({ + workflowId: process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID!, + async getClientToken() { + const res = await fetch("/api/chatkit/token", { method: "POST" }); + const { clientSecret } = await res.json(); + return clientSecret; + }, + }); + } + + // custom backend + return createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + }, [mode, accessToken]); + + return
{/* Chat UI based on client */}
; +} +``` + +--- + +## Example 11 – Minimal React (Non-Next.js) Integration + +**Goal:** Show how to adapt to a plain React/Vite setup. + +```tsx +// src/ChatKitWidget.tsx +"use client"; + +import React, { useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type Props = { + accessToken: string; +}; + +export function ChatKitWidget({ accessToken }: Props) { + const client = useMemo(() => { + return createChatKitClient({ + api: { + url: import.meta.env.VITE_CHATKIT_API_URL, + fetch: async (url, options) => { + const res = await fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + return res; + }, + uploadStrategy: { + type: "direct", + uploadUrl: import.meta.env.VITE_CHATKIT_UPLOAD_URL, + }, + domainKey: import.meta.env.VITE_CHATKIT_DOMAIN_KEY, + }, + }); + }, [accessToken]); + + return
{/* Chat UI */}
; +} +``` + +```tsx +// src/App.tsx +import { useState } from "react"; +import { ChatKitWidget } from "./ChatKitWidget"; + +function App() { + const [token] = useState("FAKE_TOKEN_FOR_DEV_ONLY"); + return ( +
+

React + ChatKit

+ +
+ ); +} + +export default App; +``` + +These examples together cover a full range of **frontend ChatKit patterns** +for teaching, debugging, and production integration. diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/reference.md b/.claude/skills/openai-chatkit-frontend-embed-skill/reference.md new file mode 100644 index 0000000..92008bd --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/reference.md @@ -0,0 +1,356 @@ +# OpenAI ChatKit – Frontend Embed Reference + +This reference document supports the `openai-chatkit-frontend-embed` Skill. +It standardizes **how you embed and configure ChatKit UI in a web frontend** +(Next.js / React / TS) for both **hosted workflows** and **custom backend** +setups. + +The goal: give students and developers a **single, opinionated pattern** for +wiring ChatKit into their apps in a secure and maintainable way. + +--- + +## 1. Scope of This Reference + +This file focuses on the **frontend layer only**: + +- How to install and import ChatKit JS/React packages. +- How to configure ChatKit for: + - Hosted workflows (Agent Builder). + - Custom backend (`api.url`, `fetch`, `uploadStrategy`, `domainKey`). +- How to pass auth and metadata from frontend → backend. +- How to debug common UI problems. + +Anything related to **ChatKit backend behavior** (Python, Agents SDK, tools, +business logic, etc.) belongs in the backend Skill/reference. + +--- + +## 2. Typical Frontend Stack Assumptions + +This reference assumes a modern TypeScript stack, for example: + +- **Next.js (App Router)** or +- **React (Vite/CRA)** + +with: + +- `NODE_ENV`-style environment variables (e.g. `NEXT_PUBLIC_*`). +- A separate **backend** domain or route (e.g. `https://api.example.com` + or `/api/chatkit` proxied to a backend). + +We treat ChatKit’s official package(s) as the source of truth for: + +- Import paths, +- Hooks/components, +- Config shapes. + +When ChatKit’s official API changes, update this reference accordingly. + +--- + +## 3. Installation & Basic Imports + +You will usually install a ChatKit package from npm, for example: + +```bash +npm install @openai/chatkit +# or a React-specific package such as: +npm install @openai/chatkit-react +``` + +> Note: Package names can evolve. Always confirm the exact name in the +> official ChatKit docs for your version. + +Basic patterns: + +```ts +// Example: using a ChatKit client factory or React provider +import { createChatKitClient } from "@openai/chatkit"; // example name +// or +import { ChatKitProvider, ChatKitWidget } from "@openai/chatkit-react"; +``` + +This Skill and reference do **not** invent APIs; they adapt to whichever +client/React API the docs specify for the version you are using. + +--- + +## 4. Two Main Modes: Hosted vs Custom Backend + +### 4.1 Hosted Workflow Mode (Agent Builder) + +In this mode: + +- ChatKit UI talks directly to OpenAI’s backend. +- Your frontend needs: + - A **workflow ID** (from Agent Builder, like `wf_...`). + - A **client token** or client secret that your backend mints. +- The backend endpoint (e.g. `/api/chatkit/token`) usually: + - Authenticates the user, + - Calls OpenAI to create a short-lived token, + - Sends that token back to the frontend. + +Frontend config shape (conceptual): + +```ts +const client = createChatKitClient({ + workflowId: process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID!, + async getClientToken() { + const res = await fetch("/api/chatkit/token", { credentials: "include" }); + if (!res.ok) throw new Error("Failed to fetch ChatKit token"); + const { clientSecret } = await res.json(); + return clientSecret; + }, + // domainKey, theme, etc. +}); +``` + +The logic of the conversation (tools, multi-agent flows, etc.) lives +primarily in **Agent Builder**, not in your code. + +### 4.2 Custom Backend Mode (Your Own Server) + +In this mode: + +- ChatKit UI talks to **your backend** instead of OpenAI directly. +- Frontend config uses a custom `api.url` and usually a custom `fetch`. + +High-level shape: + +```ts +const client = createChatKitClient({ + api: { + url: "https://api.example.com/chatkit/api", + fetch: async (url, options) => { + const accessToken = await getAccessTokenSomehow(); + return fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${accessToken}`, + }, + credentials: "include", + }); + }, + uploadStrategy: { + type: "direct", + uploadUrl: "https://api.example.com/chatkit/api/upload", + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY, + }, + // other ChatKit options... +}); +``` + +In this setup: + +- Your **backend** validates auth and talks to the Agents SDK. +- ChatKit UI stays “dumb” about models/tools and just displays messages. + +**This reference prefers custom backend mode** for advanced use cases, +especially when using the Agents SDK with OpenAI/Gemini. + +--- + +## 5. Core Config Concepts + +Regardless of the exact ChatKit API, several config concepts recur. + +### 5.1 api.url + +- URL where the frontend sends ChatKit events. +- In custom backend mode it should point to your backend route, e.g.: + - `https://api.example.com/chatkit/api` (public backend), + - `/api/chatkit` (Next.js API route that proxies to backend). + +You should **avoid** hardcoding environment-dependent URLs inline; instead, +use environment variables: + +```ts +const CHATKIT_API_URL = + process.env.NEXT_PUBLIC_CHATKIT_API_URL ?? "/api/chatkit"; +``` + +### 5.2 api.fetch (Custom Fetch) + +Custom fetch allows you to inject auth and metadata: + +```ts +fetch: async (url, options) => { + const token = await getAccessToken(); + return fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${token}`, + "X-User-Id": user.id, + "X-Tenant-Id": tenantId, + }, + credentials: "include", + }); +} +``` + +Key rules: + +- **Never** send raw OpenAI/Gemini API keys from the frontend. +- Only send short-lived access tokens or session cookies. +- If multi-tenant, send tenant identifiers as headers, not in query strings. + +### 5.3 uploadStrategy + +Controls how file uploads are handled. In custom backend mode you typically +use **direct upload** to your backend: + +```ts +uploadStrategy: { + type: "direct", + uploadUrl: CHATKIT_UPLOAD_URL, // e.g. "/api/chatkit/upload" +} +``` + +Backend responsibilities: + +- Accept `multipart/form-data`, +- Store files (disk, S3, etc.), +- Return a JSON body with a public URL and metadata expected by ChatKit. + +### 5.4 domainKey & Allowlisted Domains + +- ChatKit often requires a **domain allowlist** to decide which origins + are allowed to render the widget. +- A `domainKey` (or similar) is usually provided by OpenAI UI / dashboard. + +On the frontend: + +- Store it in `NEXT_PUBLIC_CHATKIT_DOMAIN_KEY` (or similar). +- Pass it through ChatKit config: + + ```ts + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY, + ``` + +If the widget is blank or disappears, check: + +- Is the origin (e.g. `https://app.example.com`) allowlisted? +- Is the `domainKey` correct and present? + +--- + +## 6. Recommended Next.js Organization + +For Next.js App Router (TypeScript), a common structure: + +```text +src/ + app/ + chat/ + page.tsx # Chat page using ChatKit + components/ + chatkit/ + ChatKitProvider.tsx + ChatKitWidget.tsx + chatkitClient.ts # optional client factory +``` + +### 6.1 ChatKitProvider.tsx (Conceptual) + +- Wraps your chat tree with the ChatKit context/provider. +- Injects ChatKit client config in one place. + +### 6.2 ChatKitWidget.tsx + +- A focused component that renders the actual Chat UI. +- Receives props like `user`, `tenantId`, optional initial messages. + +### 6.3 Environment Variables + +Use `NEXT_PUBLIC_...` only for **non-secret** values: + +- `NEXT_PUBLIC_CHATKIT_DOMAIN_KEY` +- `NEXT_PUBLIC_CHATKIT_API_URL` +- `NEXT_PUBLIC_CHATKIT_WORKFLOW_ID` (if using hosted workflows) + +Secrets belong on the backend side. + +--- + +## 7. Debugging & Common Issues + +### 7.1 Widget Not Showing / Blank + +Checklist: + +1. Check browser console for errors. +2. Confirm correct import paths / package versions. +3. Verify **domain allowlist** and `domainKey` configuration. +4. Check network tab: + - Are `chatkit` requests being sent? + - Any 4xx/5xx or CORS errors? +5. If using custom backend: + - Confirm the backend route exists and returns a valid response shape. + +### 7.2 “Loading…” Never Finishes + +- Usually indicates backend is not returning expected structure or stream. +- Add logging to backend for incoming ChatKit events and outgoing responses. +- Temporarily log responses on the frontend to inspect their shape. + +### 7.3 File Uploads Fail + +- Ensure `uploadUrl` points to a backend route that accepts `multipart/form-data`. +- Check response body shape matches ChatKit’s expectation (URL field, etc.). +- Inspect network tab to confirm request/response. + +### 7.4 Auth / 401 Errors + +- Confirm that your custom `fetch` attaches the correct token or cookie. +- Confirm backend checks that token and does not fail with generic 401/403. +- In dev, log incoming headers on backend for debugging (but never log + secrets to console in production). + +--- + +## 8. Evolving with ChatKit Versions + +ChatKit’s API may change over time (prop names, hooks, config keys). To keep +this Skill and your code up to date: + +- Treat **official ChatKit docs** as the top source of truth for frontend + API details. +- If you have ChatKit docs via MCP (e.g. `chatkit/frontend/latest.md`, + `chatkit/changelog.md`), prefer them over older examples. +- When you detect a mismatch (e.g. a prop is renamed or removed): + - Update your local templates/components. + - Update this reference file. + +A good practice is to maintain a short local changelog next to this file +documenting which ChatKit version the examples were last validated against. + +--- + +## 9. Teaching & Best Practices Summary + +When using this Skill and reference to teach students or onboard teammates: + +- Start with a **simple, working embed**: + - Hosted workflow mode OR + - Custom backend that just echoes messages. +- Then layer in: + - Auth header injection, + - File uploads, + - Multi-tenant headers, + - Theming and layout. + +Enforce these best practices: + +- No API keys in frontend code. +- Single, centralized ChatKit config (not scattered across components). +- Clear separation of concerns: + - Frontend: UI + ChatKit config. + - Backend: Auth + business logic + Agents SDK. + +By following this reference, the `openai-chatkit-frontend-embed` Skill can +generate **consistent, secure, and maintainable** ChatKit frontend code +across projects. diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitProvider.tsx b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitProvider.tsx new file mode 100644 index 0000000..894eb50 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitProvider.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React, { createContext, useContext, useMemo } from "react"; +import { createChatKitClient } from "@openai/chatkit"; + +type ChatKitContextValue = { + client: any; +}; + +const ChatKitContext = createContext(null); + +type Props = { + accessToken: string; + children: React.ReactNode; +}; + +export function ChatKitProvider({ accessToken, children }: Props) { + const value = useMemo(() => { + const client = createChatKitClient({ + api: { + url: process.env.NEXT_PUBLIC_CHATKIT_API_URL!, + fetch: async (url, options) => { + return fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${accessToken}`, + }, + }); + }, + uploadStrategy: { + type: "direct", + uploadUrl: process.env.NEXT_PUBLIC_CHATKIT_UPLOAD_URL!, + }, + domainKey: process.env.NEXT_PUBLIC_CHATKIT_DOMAIN_KEY!, + }, + }); + return { client }; + }, [accessToken]); + + return ( + + {children} + + ); +} + +export function useChatKit() { + const ctx = useContext(ChatKitContext); + if (!ctx) throw new Error("useChatKit must be used in provider"); + return ctx; +} diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitWidget.tsx b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitWidget.tsx new file mode 100644 index 0000000..d83986c --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/ChatKitWidget.tsx @@ -0,0 +1,16 @@ +"use client"; + +import React from "react"; +import { useChatKit } from "./ChatKitProvider"; + +export function ChatKitWidget() { + const { client } = useChatKit(); + + return ( +
+

+ ChatKit UI will render here with client instance. +

+
+ ); +} diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/FloatingChatButton.tsx b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/FloatingChatButton.tsx new file mode 100644 index 0000000..bae4000 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/FloatingChatButton.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useState } from "react"; +import { ChatKitWidget } from "./ChatKitWidget"; + +export function FloatingChatButton({ accessToken }: { accessToken: string }) { + const [open, setOpen] = useState(false); + + return ( + <> + {open && ( +
+ +
+ )} + + + + ); +} diff --git a/.claude/skills/openai-chatkit-frontend-embed-skill/templates/makeFetch.ts b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/makeFetch.ts new file mode 100644 index 0000000..882dc78 --- /dev/null +++ b/.claude/skills/openai-chatkit-frontend-embed-skill/templates/makeFetch.ts @@ -0,0 +1,11 @@ +export function makeChatKitFetch(accessToken: string, extras?: Record) { + return async (url: string, options: RequestInit) => { + const headers: HeadersInit = { + ...(options.headers || {}), + Authorization: `Bearer ${accessToken}`, + ...(extras || {}), + }; + + return fetch(url, { ...options, headers }); + }; +} diff --git a/.claude/skills/openai-chatkit-gemini/SKILL.md b/.claude/skills/openai-chatkit-gemini/SKILL.md new file mode 100644 index 0000000..9c19afa --- /dev/null +++ b/.claude/skills/openai-chatkit-gemini/SKILL.md @@ -0,0 +1,473 @@ +--- +name: openai-chatkit-gemini +description: > + Integrate Google Gemini models (gemini-2.5-flash, gemini-2.0-flash, etc.) with + OpenAI Agents SDK and ChatKit. Use this Skill when building ChatKit backends + powered by Gemini via the OpenAI-compatible endpoint or LiteLLM integration. +--- + +# OpenAI Agents SDK + Gemini Integration Skill + +You are a **Gemini integration specialist** for OpenAI Agents SDK and ChatKit backends. + +Your job is to help users integrate **Google Gemini models** with the OpenAI Agents SDK +for use in ChatKit custom backends or standalone agent applications. + +## 1. When to Use This Skill + +Use this Skill **whenever**: + +- The user mentions: + - "Gemini with Agents SDK" + - "gemini-2.5-flash" or any Gemini model + - "ChatKit with Gemini" + - "non-OpenAI models in Agents SDK" + - "LiteLLM integration" + - "OpenAI-compatible endpoint for Gemini" +- Or asks to: + - Configure Gemini as the model provider for an agent + - Switch from OpenAI to Gemini in their backend + - Use Google's AI models with the OpenAI Agents SDK + - Debug Gemini-related issues in their ChatKit backend + +## 2. Integration Methods (Choose One) + +There are **two primary methods** to integrate Gemini with OpenAI Agents SDK: + +### Method 1: OpenAI-Compatible Endpoint (Recommended) + +Uses Google's official OpenAI-compatible API endpoint directly. + +**Pros:** +- Direct integration, no extra dependencies +- Full control over configuration +- Works with existing OpenAI SDK patterns + +**Base URL:** `https://generativelanguage.googleapis.com/v1beta/openai/` + +### Method 2: LiteLLM Integration + +Uses LiteLLM as an abstraction layer for 100+ model providers. + +**Pros:** +- Easy provider switching +- Consistent interface across providers +- Built-in retry and fallback logic + +**Install:** `pip install 'openai-agents[litellm]'` + +## 3. Core Architecture + +### 3.1 Environment Variables + +```text +# Required for Gemini +GEMINI_API_KEY=your-gemini-api-key + +# Provider selection +LLM_PROVIDER=gemini + +# Model selection +GEMINI_DEFAULT_MODEL=gemini-2.5-flash + +# Optional: For LiteLLM method +LITELLM_LOG=DEBUG +``` + +### 3.2 Model Factory Pattern (MANDATORY) + +**ALWAYS use a centralized factory function for model creation:** + +```python +# agents/factory.py +import os +from openai import AsyncOpenAI +from agents import OpenAIChatCompletionsModel + +# Gemini OpenAI-compatible base URL +GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/" + +def create_model(): + """Create model instance based on LLM_PROVIDER environment variable. + + Returns: + Model instance compatible with OpenAI Agents SDK. + """ + provider = os.getenv("LLM_PROVIDER", "openai").lower() + + if provider == "gemini": + return create_gemini_model() + + # Default: OpenAI + return create_openai_model() + + +def create_gemini_model(model_name: str | None = None): + """Create Gemini model via OpenAI-compatible endpoint. + + Args: + model_name: Gemini model ID. Defaults to GEMINI_DEFAULT_MODEL env var. + + Returns: + OpenAIChatCompletionsModel configured for Gemini. + """ + api_key = os.getenv("GEMINI_API_KEY") + if not api_key: + raise ValueError("GEMINI_API_KEY environment variable is required") + + model = model_name or os.getenv("GEMINI_DEFAULT_MODEL", "gemini-2.5-flash") + + client = AsyncOpenAI( + api_key=api_key, + base_url=GEMINI_BASE_URL, + ) + + return OpenAIChatCompletionsModel( + model=model, + openai_client=client, + ) + + +def create_openai_model(model_name: str | None = None): + """Create OpenAI model (default provider). + + Args: + model_name: OpenAI model ID. Defaults to OPENAI_DEFAULT_MODEL env var. + + Returns: + OpenAIChatCompletionsModel configured for OpenAI. + """ + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise ValueError("OPENAI_API_KEY environment variable is required") + + model = model_name or os.getenv("OPENAI_DEFAULT_MODEL", "gpt-4o-mini") + + client = AsyncOpenAI(api_key=api_key) + + return OpenAIChatCompletionsModel( + model=model, + openai_client=client, + ) +``` + +### 3.3 LiteLLM Alternative Factory + +```python +# agents/factory_litellm.py +import os +from agents.extensions.models.litellm_model import LitellmModel + +def create_model(): + """Create model using LiteLLM for provider abstraction.""" + provider = os.getenv("LLM_PROVIDER", "openai").lower() + + if provider == "gemini": + model_id = os.getenv("GEMINI_DEFAULT_MODEL", "gemini-2.5-flash") + # LiteLLM format: provider/model + return LitellmModel(model_id=f"gemini/{model_id}") + + # Default: OpenAI + model_id = os.getenv("OPENAI_DEFAULT_MODEL", "gpt-4o-mini") + return LitellmModel(model_id=f"openai/{model_id}") +``` + +## 4. Supported Gemini Models + +| Model ID | Description | Recommended Use | +|----------|-------------|-----------------| +| `gemini-2.5-flash` | Latest fast model | **Default choice** - best speed/quality | +| `gemini-2.5-pro` | Most capable model | Complex reasoning tasks | +| `gemini-2.0-flash` | Previous generation fast | Fallback if 2.5 has issues | +| `gemini-2.0-flash-lite` | Lightweight variant | Cost-sensitive applications | + +**IMPORTANT:** Use stable model versions in production. Preview models (e.g., +`gemini-2.5-flash-preview-05-20`) may have compatibility issues with tool calling. + +## 5. Agent Creation with Gemini + +### 5.1 Basic Agent + +```python +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="gemini-assistant", + model=create_model(), # Uses factory to get Gemini + instructions="""You are a helpful assistant powered by Gemini. + Be concise and accurate in your responses.""", +) + +# Synchronous execution +result = Runner.run_sync(starting_agent=agent, input="Hello!") +print(result.final_output) +``` + +### 5.2 Agent with Tools + +```python +from agents import Agent, Runner, function_tool +from agents.factory import create_model + +@function_tool +def get_weather(city: str) -> str: + """Get current weather for a city.""" + # Implementation here + return f"Weather in {city}: Sunny, 72°F" + +agent = Agent( + name="weather-assistant", + model=create_model(), + instructions="""You are a weather assistant. + Use the get_weather tool when asked about weather. + IMPORTANT: Do not format tool results as JSON - just describe them naturally.""", + tools=[get_weather], +) + +result = Runner.run_sync(starting_agent=agent, input="What's the weather in Tokyo?") +``` + +### 5.3 Streaming Agent + +```python +import asyncio +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="streaming-gemini", + model=create_model(), + instructions="You are a helpful assistant. Respond in detail.", +) + +async def stream_response(user_input: str): + result = Runner.run_streamed(agent, user_input) + + async for event in result.stream_events(): + if hasattr(event, 'data') and hasattr(event.data, 'delta'): + print(event.data.delta, end="", flush=True) + + print() # Newline at end + return await result.final_output + +asyncio.run(stream_response("Explain quantum computing")) +``` + +## 6. ChatKit Integration with Gemini + +### 6.1 ChatKitServer with Gemini + +```python +# server.py +from chatkit.server import ChatKitServer +from chatkit.stores import FileStore +from chatkit.agents import AgentContext, simple_to_agent_input, stream_agent_response +from agents import Agent, Runner +from agents.factory import create_model + +class GeminiChatServer(ChatKitServer): + def __init__(self): + self.store = FileStore(base_path="./chat_data") + self.agent = self._create_agent() + + def _create_agent(self) -> Agent: + return Agent( + name="gemini-chatkit-agent", + model=create_model(), # Gemini via factory + instructions="""You are a helpful assistant in a ChatKit interface. + Keep responses concise and user-friendly. + When tools return data, DO NOT reformat it - it displays automatically.""", + tools=[...], # Your MCP tools + ) + + async def respond(self, thread, input, context): + agent_context = AgentContext( + thread=thread, + store=self.store, + request_context=context, + ) + + agent_input = await simple_to_agent_input(input) if input else [] + + result = Runner.run_streamed( + self.agent, + agent_input, + context=agent_context, + ) + + async for event in stream_agent_response(agent_context, result): + yield event +``` + +### 6.2 FastAPI Endpoint + +```python +# main.py +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from server import GeminiChatServer + +app = FastAPI() +server = GeminiChatServer() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.post("/chatkit/api") +async def chatkit_api(request: Request): + # Auth validation here + body = await request.json() + thread_id = body.get("thread_id", "default") + user_message = body.get("message", {}).get("content", "") + + # Build thread and input objects + from chatkit.server import ThreadMetadata, UserMessageItem + thread = ThreadMetadata(id=thread_id) + input_item = UserMessageItem(content=user_message) if user_message else None + context = {"user_id": "guest"} # Add auth context here + + async def generate(): + async for event in server.respond(thread, input_item, context): + yield f"data: {event.model_dump_json()}\n\n" + + return StreamingResponse(generate(), media_type="text/event-stream") +``` + +## 7. Known Issues & Workarounds + +### 7.1 AttributeError with Tools (Fixed in SDK) + +**Issue:** Some Gemini preview models return `None` for `choices[0].message` +when tools are specified, causing `AttributeError`. + +**Affected Models:** `gemini-2.5-flash-preview-05-20` and similar previews + +**Solution:** +1. Use stable model versions (e.g., `gemini-2.5-flash` without preview suffix) +2. Update to latest `openai-agents` package (fix merged in PR #746) + +### 7.2 Structured Output Limitations + +**Issue:** Gemini may not fully support `response_format` with `json_schema`. + +**Solution:** Use instruction-based JSON formatting instead: + +```python +agent = Agent( + name="json-agent", + model=create_model(), + instructions="""Always respond with valid JSON in this format: + {"result": "your answer", "confidence": 0.0-1.0} + Do not include any text outside the JSON object.""", +) +``` + +### 7.3 Tool Calling Differences + +**Issue:** Gemini's tool calling may behave slightly differently than OpenAI's. + +**Best Practices:** +- Keep tool descriptions clear and concise +- Avoid complex nested parameter schemas +- Test tools thoroughly with Gemini before production +- Add explicit instructions about tool usage in agent instructions + +## 8. Debugging Guide + +### 8.1 Connection Issues + +```python +# Test Gemini connection +import os +from openai import AsyncOpenAI +import asyncio + +async def test_gemini(): + client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url="https://generativelanguage.googleapis.com/v1beta/openai/", + ) + + response = await client.chat.completions.create( + model="gemini-2.5-flash", + messages=[{"role": "user", "content": "Hello!"}], + ) + print(response.choices[0].message.content) + +asyncio.run(test_gemini()) +``` + +### 8.2 Common Error Messages + +| Error | Cause | Fix | +|-------|-------|-----| +| `401 Unauthorized` | Invalid API key | Check GEMINI_API_KEY | +| `404 Not Found` | Wrong model name | Use valid model ID | +| `AttributeError: 'NoneType'...` | Preview model issue | Use stable model | +| `response_format` error | Structured output unsupported | Remove json_schema | + +### 8.3 Enable Debug Logging + +```python +import logging +logging.basicConfig(level=logging.DEBUG) + +# For LiteLLM +import os +os.environ["LITELLM_LOG"] = "DEBUG" +``` + +## 9. Best Practices + +1. **Always use the factory pattern** - Never hardcode model configuration +2. **Use stable model versions** - Avoid preview/experimental models in production +3. **Handle provider switching** - Design for easy OpenAI/Gemini switching +4. **Test tool calling** - Verify tools work correctly with Gemini +5. **Monitor rate limits** - Gemini has different quotas than OpenAI +6. **Keep SDK updated** - New fixes for Gemini compatibility are released regularly + +## 10. Quick Reference + +### Environment Setup + +```bash +# .env file +LLM_PROVIDER=gemini +GEMINI_API_KEY=your-api-key +GEMINI_DEFAULT_MODEL=gemini-2.5-flash +``` + +### Minimal Agent + +```python +from agents import Agent, Runner +from openai import AsyncOpenAI +from agents import OpenAIChatCompletionsModel + +client = AsyncOpenAI( + api_key="your-gemini-api-key", + base_url="https://generativelanguage.googleapis.com/v1beta/openai/", +) + +agent = Agent( + name="gemini-agent", + model=OpenAIChatCompletionsModel(model="gemini-2.5-flash", openai_client=client), + instructions="You are a helpful assistant.", +) + +result = Runner.run_sync(agent, "Hello!") +print(result.final_output) +``` + +## 11. Related Skills + +- `openai-chatkit-backend-python` - Full ChatKit backend patterns +- `openai-chatkit-frontend-embed-skill` - Frontend widget integration +- `fastapi` - Backend framework patterns diff --git a/.claude/skills/openai-chatkit-gemini/examples/basic-agent.md b/.claude/skills/openai-chatkit-gemini/examples/basic-agent.md new file mode 100644 index 0000000..71f37e0 --- /dev/null +++ b/.claude/skills/openai-chatkit-gemini/examples/basic-agent.md @@ -0,0 +1,438 @@ +# Basic Gemini Agent Examples + +Practical examples for creating agents with Gemini models using the OpenAI Agents SDK. + +## Example 1: Minimal Gemini Agent + +The simplest possible Gemini agent. + +```python +# minimal_agent.py +import os +from openai import AsyncOpenAI +from agents import Agent, Runner, OpenAIChatCompletionsModel + +# Configure Gemini client +client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url="https://generativelanguage.googleapis.com/v1beta/openai/", +) + +# Create model +model = OpenAIChatCompletionsModel( + model="gemini-2.5-flash", + openai_client=client, +) + +# Create agent +agent = Agent( + name="gemini-assistant", + model=model, + instructions="You are a helpful assistant. Be concise and accurate.", +) + +# Run synchronously +result = Runner.run_sync(agent, "What is the capital of France?") +print(result.final_output) +``` + +## Example 2: Factory-Based Agent + +Using the factory pattern for clean configuration. + +```python +# agents/factory.py +import os +from openai import AsyncOpenAI +from agents import OpenAIChatCompletionsModel + +GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/" + + +def create_model(): + """Create model based on LLM_PROVIDER environment variable.""" + provider = os.getenv("LLM_PROVIDER", "openai").lower() + + if provider == "gemini": + client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url=GEMINI_BASE_URL, + ) + return OpenAIChatCompletionsModel( + model=os.getenv("GEMINI_DEFAULT_MODEL", "gemini-2.5-flash"), + openai_client=client, + ) + + # Default: OpenAI + client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) + return OpenAIChatCompletionsModel( + model=os.getenv("OPENAI_DEFAULT_MODEL", "gpt-4o-mini"), + openai_client=client, + ) +``` + +```python +# main.py +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="factory-agent", + model=create_model(), + instructions="You are a helpful assistant.", +) + +result = Runner.run_sync(agent, "Hello!") +print(result.final_output) +``` + +```bash +# .env +LLM_PROVIDER=gemini +GEMINI_API_KEY=your-api-key +GEMINI_DEFAULT_MODEL=gemini-2.5-flash +``` + +## Example 3: Async Agent + +Asynchronous agent execution. + +```python +# async_agent.py +import asyncio +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="async-gemini", + model=create_model(), + instructions="You are a helpful assistant.", +) + + +async def main(): + # Single async call + result = await Runner.run(agent, "Tell me a short joke") + print(result.final_output) + + # Multiple concurrent calls + tasks = [ + Runner.run(agent, "What is 2+2?"), + Runner.run(agent, "What color is the sky?"), + Runner.run(agent, "Name a fruit"), + ] + results = await asyncio.gather(*tasks) + + for r in results: + print(f"- {r.final_output}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Example 4: Streaming Agent + +Real-time streaming responses. + +```python +# streaming_agent.py +import asyncio +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="streaming-gemini", + model=create_model(), + instructions="You are a storyteller. Tell engaging stories.", +) + + +async def stream_response(prompt: str): + result = Runner.run_streamed(agent, prompt) + + async for event in result.stream_events(): + if hasattr(event, "data"): + if hasattr(event.data, "delta"): + print(event.data.delta, end="", flush=True) + + print() # Newline at end + final = await result.final_output + return final + + +async def main(): + print("Streaming response:\n") + await stream_response("Tell me a very short story about a robot") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Example 5: Agent with Custom Settings + +Configuring temperature and other model parameters. + +```python +# custom_settings_agent.py +from agents import Agent, Runner, ModelSettings +from agents.factory import create_model + +# Creative agent with high temperature +creative_agent = Agent( + name="creative-writer", + model=create_model(), + model_settings=ModelSettings( + temperature=0.9, + max_tokens=2048, + top_p=0.95, + ), + instructions="""You are a creative writer. + Generate unique, imaginative content. + Don't be afraid to be unconventional.""", +) + +# Precise agent with low temperature +precise_agent = Agent( + name="fact-checker", + model=create_model(), + model_settings=ModelSettings( + temperature=0.1, + max_tokens=1024, + ), + instructions="""You are a fact-focused assistant. + Provide accurate, verified information only. + If uncertain, say so.""", +) + +# Run both +creative_result = Runner.run_sync( + creative_agent, + "Write a unique metaphor for learning" +) +print(f"Creative: {creative_result.final_output}\n") + +precise_result = Runner.run_sync( + precise_agent, + "What is the speed of light in vacuum?" +) +print(f"Precise: {precise_result.final_output}") +``` + +## Example 6: Conversation Agent + +Multi-turn conversation handling. + +```python +# conversation_agent.py +import asyncio +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="conversational-gemini", + model=create_model(), + instructions="""You are a friendly conversational assistant. + Remember context from previous messages. + Be engaging and ask follow-up questions.""", +) + + +async def chat(): + conversation_history = [] + + print("Chat with Gemini (type 'quit' to exit)\n") + + while True: + user_input = input("You: ").strip() + + if user_input.lower() == "quit": + print("Goodbye!") + break + + if not user_input: + continue + + # Build input with history + messages = conversation_history + [ + {"role": "user", "content": user_input} + ] + + result = await Runner.run(agent, messages) + response = result.final_output + + # Update history + conversation_history.append({"role": "user", "content": user_input}) + conversation_history.append({"role": "assistant", "content": response}) + + print(f"Gemini: {response}\n") + + +if __name__ == "__main__": + asyncio.run(chat()) +``` + +## Example 7: Error Handling + +Robust error handling for production. + +```python +# robust_agent.py +import asyncio +from openai import ( + APIError, + AuthenticationError, + RateLimitError, + APIConnectionError, +) +from agents import Agent, Runner +from agents.factory import create_model + +agent = Agent( + name="robust-gemini", + model=create_model(), + instructions="You are a helpful assistant.", +) + + +async def safe_query(prompt: str, max_retries: int = 3) -> str: + """Execute agent query with error handling and retries.""" + last_error = None + + for attempt in range(max_retries): + try: + result = await Runner.run(agent, prompt) + return result.final_output + + except AuthenticationError: + # Don't retry auth errors + raise ValueError("Invalid GEMINI_API_KEY") + + except RateLimitError as e: + last_error = e + if attempt < max_retries - 1: + wait = 2 ** attempt + print(f"Rate limited, waiting {wait}s...") + await asyncio.sleep(wait) + + except APIConnectionError as e: + last_error = e + if attempt < max_retries - 1: + wait = 1 + print(f"Connection error, retrying in {wait}s...") + await asyncio.sleep(wait) + + except APIError as e: + last_error = e + print(f"API error: {e}") + break + + raise ValueError(f"Failed after {max_retries} attempts: {last_error}") + + +async def main(): + try: + response = await safe_query("What is 2+2?") + print(f"Response: {response}") + except ValueError as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Example 8: Testing Gemini Connection + +Verify your setup works before building agents. + +```python +# test_connection.py +import os +import asyncio +from openai import AsyncOpenAI + +GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/" + + +async def test_gemini_connection(): + """Test basic Gemini API connectivity.""" + api_key = os.getenv("GEMINI_API_KEY") + + if not api_key: + print("ERROR: GEMINI_API_KEY not set") + return False + + try: + client = AsyncOpenAI( + api_key=api_key, + base_url=GEMINI_BASE_URL, + ) + + response = await client.chat.completions.create( + model="gemini-2.5-flash", + messages=[{"role": "user", "content": "Say 'Hello World'"}], + max_tokens=50, + ) + + content = response.choices[0].message.content + print(f"SUCCESS: {content}") + return True + + except Exception as e: + print(f"ERROR: {e}") + return False + + +async def test_streaming(): + """Test streaming capability.""" + api_key = os.getenv("GEMINI_API_KEY") + + client = AsyncOpenAI( + api_key=api_key, + base_url=GEMINI_BASE_URL, + ) + + print("Testing streaming: ", end="") + + stream = await client.chat.completions.create( + model="gemini-2.5-flash", + messages=[{"role": "user", "content": "Count to 5"}], + stream=True, + ) + + async for chunk in stream: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) + + print("\nStreaming: OK") + + +if __name__ == "__main__": + print("Testing Gemini connection...\n") + asyncio.run(test_gemini_connection()) + print() + asyncio.run(test_streaming()) +``` + +## Running the Examples + +1. Set up environment: +```bash +export GEMINI_API_KEY="your-api-key" +export LLM_PROVIDER="gemini" +export GEMINI_DEFAULT_MODEL="gemini-2.5-flash" +``` + +2. Install dependencies: +```bash +pip install openai-agents openai +``` + +3. Run any example: +```bash +python minimal_agent.py +python streaming_agent.py +python test_connection.py +``` diff --git a/.claude/skills/openai-chatkit-gemini/examples/chatkit-integration.md b/.claude/skills/openai-chatkit-gemini/examples/chatkit-integration.md new file mode 100644 index 0000000..b59f3d3 --- /dev/null +++ b/.claude/skills/openai-chatkit-gemini/examples/chatkit-integration.md @@ -0,0 +1,631 @@ +# ChatKit Integration with Gemini Examples + +Complete examples for building ChatKit backends powered by Gemini models. + +## Example 1: Minimal ChatKit Backend + +The simplest ChatKit backend with Gemini. + +```python +# main.py +import os +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware + +from openai import AsyncOpenAI +from agents import Agent, Runner, OpenAIChatCompletionsModel + +# Initialize FastAPI +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Configure Gemini +GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/" + +client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url=GEMINI_BASE_URL, +) + +model = OpenAIChatCompletionsModel( + model="gemini-2.5-flash", + openai_client=client, +) + +# Create agent +agent = Agent( + name="chatkit-gemini", + model=model, + instructions="You are a helpful assistant. Be concise and friendly.", +) + + +@app.post("/chatkit/api") +async def chatkit_endpoint(request: Request): + """Handle ChatKit API requests.""" + event = await request.json() + user_message = event.get("message", {}).get("content", "") + + # Non-streaming response + result = Runner.run_sync(agent, user_message) + + return { + "type": "message", + "content": result.final_output, + "done": True, + } + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Example 2: Streaming ChatKit Backend + +Real-time streaming responses with Gemini. + +```python +# streaming_backend.py +import os +import json +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware + +from openai import AsyncOpenAI +from agents import Agent, Runner, OpenAIChatCompletionsModel + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Gemini configuration +client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url="https://generativelanguage.googleapis.com/v1beta/openai/", +) + +model = OpenAIChatCompletionsModel(model="gemini-2.5-flash", openai_client=client) + +agent = Agent( + name="streaming-gemini", + model=model, + instructions="You are a helpful assistant. Provide detailed responses.", +) + + +async def generate_stream(user_message: str): + """Generate SSE stream from agent response.""" + result = Runner.run_streamed(agent, user_message) + + async for event in result.stream_events(): + if hasattr(event, "data") and hasattr(event.data, "delta"): + chunk = event.data.delta + if chunk: + yield f"data: {json.dumps({'text': chunk})}\n\n" + + # Signal completion + yield f"data: {json.dumps({'done': True})}\n\n" + + +@app.post("/chatkit/api") +async def chatkit_streaming(request: Request): + """Handle ChatKit requests with streaming.""" + event = await request.json() + user_message = event.get("message", {}).get("content", "") + + return StreamingResponse( + generate_stream(user_message), + media_type="text/event-stream", + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Example 3: Full ChatKit Server with Tools + +Complete ChatKitServer implementation with Gemini and widget streaming. + +```python +# chatkit_server.py +import os +from typing import AsyncIterator, Any +from chatkit.server import ChatKitServer, ThreadMetadata, UserMessageItem, ThreadStreamEvent +from chatkit.stores import FileStore +from chatkit.agents import AgentContext, simple_to_agent_input, stream_agent_response +from chatkit.widgets import ListView, ListViewItem, Text, Row, Col, Badge + +from openai import AsyncOpenAI +from agents import Agent, Runner, OpenAIChatCompletionsModel, function_tool, RunContextWrapper + + +# Configure Gemini +GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/" + +client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url=GEMINI_BASE_URL, +) + +model = OpenAIChatCompletionsModel( + model=os.getenv("GEMINI_DEFAULT_MODEL", "gemini-2.5-flash"), + openai_client=client, +) + + +# Define tools with widget streaming +@function_tool +async def list_tasks( + ctx: RunContextWrapper[AgentContext], + status: str = "all", +) -> None: + """List user's tasks with optional status filter. + + Args: + ctx: Agent context. + status: Filter by 'pending', 'completed', or 'all'. + """ + # Get user from context + user_id = ctx.context.request_context.get("user_id", "guest") + + # Mock: fetch from database + tasks = [ + {"id": 1, "title": "Review PR #123", "status": "pending", "priority": "high"}, + {"id": 2, "title": "Update docs", "status": "pending", "priority": "medium"}, + {"id": 3, "title": "Fix login bug", "status": "completed", "priority": "high"}, + ] + + # Filter by status + if status != "all": + tasks = [t for t in tasks if t["status"] == status] + + # Build widget items + items = [] + for task in tasks: + icon = "checkmark.circle.fill" if task["status"] == "completed" else "circle" + color = "green" if task["status"] == "completed" else "primary" + + items.append( + ListViewItem( + children=[ + Row( + children=[ + Text(value=icon, size="lg"), + Col( + children=[ + Text( + value=task["title"], + weight="semibold", + color=color, + lineThrough=task["status"] == "completed", + ), + Text( + value=f"Priority: {task['priority']}", + size="sm", + color="secondary", + ), + ], + gap=1, + ), + Badge( + label=f"#{task['id']}", + color="secondary", + size="sm", + ), + ], + gap=3, + align="center", + ) + ] + ) + ) + + # Create widget + widget = ListView( + children=items if items else [ + ListViewItem( + children=[Text(value="No tasks found", color="secondary", italic=True)] + ) + ], + status={"text": f"Tasks ({len(tasks)})", "icon": {"name": "checklist"}}, + limit="auto", + ) + + # Stream widget to ChatKit + await ctx.context.stream_widget(widget) + + +@function_tool +async def add_task( + ctx: RunContextWrapper[AgentContext], + title: str, + priority: str = "medium", +) -> str: + """Add a new task. + + Args: + ctx: Agent context. + title: Task title. + priority: Task priority (low, medium, high). + + Returns: + Confirmation message. + """ + user_id = ctx.context.request_context.get("user_id", "guest") + + # Mock: save to database + task_id = 4 # Would be from DB + + return f"Created task #{task_id}: '{title}' with {priority} priority" + + +@function_tool +async def complete_task( + ctx: RunContextWrapper[AgentContext], + task_id: int, +) -> str: + """Mark a task as completed. + + Args: + ctx: Agent context. + task_id: ID of task to complete. + + Returns: + Confirmation message. + """ + # Mock: update in database + return f"Task #{task_id} marked as completed" + + +# Create ChatKit server +class GeminiChatServer(ChatKitServer): + def __init__(self): + self.store = FileStore(base_path="./chat_data") + self.agent = self._create_agent() + + def _create_agent(self) -> Agent: + return Agent( + name="gemini-task-assistant", + model=model, + instructions="""You are a task management assistant powered by Gemini. + + AVAILABLE TOOLS: + - list_tasks: Show user's tasks (displays automatically in a widget) + - add_task: Create a new task + - complete_task: Mark a task as done + + IMPORTANT RULES: + 1. When list_tasks is called, the data displays automatically in a widget + 2. DO NOT format task data as text/JSON - just say "Here are your tasks" + 3. Be helpful and proactive about task organization + 4. Confirm actions clearly after add_task or complete_task + """, + tools=[list_tasks, add_task, complete_task], + ) + + async def respond( + self, + thread: ThreadMetadata, + input: UserMessageItem | None, + context: Any, + ) -> AsyncIterator[ThreadStreamEvent]: + """Process user messages and stream responses.""" + + # Create agent context + agent_context = AgentContext( + thread=thread, + store=self.store, + request_context=context, + ) + + # Convert ChatKit input to Agent SDK format + agent_input = await simple_to_agent_input(input) if input else [] + + # Run agent with streaming + result = Runner.run_streamed( + self.agent, + agent_input, + context=agent_context, + ) + + # Stream response (widgets streamed by tools) + async for event in stream_agent_response(agent_context, result): + yield event + + +# FastAPI integration +from fastapi import FastAPI, Request, Header +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +server = GeminiChatServer() + + +@app.post("/chatkit/api") +async def chatkit_api( + request: Request, + authorization: str = Header(None), +): + """Handle ChatKit API requests.""" + # Extract user from auth header + user_id = "guest" + if authorization: + # Validate JWT and extract user_id + # user_id = validate_jwt(authorization) + pass + + # Parse request + body = await request.json() + + # Build thread metadata + thread = ThreadMetadata( + id=body.get("thread_id", "default"), + # Additional thread metadata + ) + + # Build input + input_data = body.get("input") + input_item = UserMessageItem( + content=input_data.get("content", ""), + ) if input_data else None + + # Context for tools + context = { + "user_id": user_id, + "request": request, + } + + async def generate(): + async for event in server.respond(thread, input_item, context): + yield f"data: {event.model_dump_json()}\n\n" + + return StreamingResponse( + generate(), + media_type="text/event-stream", + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Example 4: Provider-Switchable Backend + +Backend that can switch between OpenAI and Gemini. + +```python +# switchable_backend.py +import os +from typing import AsyncIterator +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware + +from openai import AsyncOpenAI +from agents import Agent, Runner, OpenAIChatCompletionsModel + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# Model factory +def create_model(): + """Create model based on LLM_PROVIDER environment variable.""" + provider = os.getenv("LLM_PROVIDER", "openai").lower() + + if provider == "gemini": + client = AsyncOpenAI( + api_key=os.getenv("GEMINI_API_KEY"), + base_url="https://generativelanguage.googleapis.com/v1beta/openai/", + ) + return OpenAIChatCompletionsModel( + model=os.getenv("GEMINI_DEFAULT_MODEL", "gemini-2.5-flash"), + openai_client=client, + ) + + # Default: OpenAI + client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) + return OpenAIChatCompletionsModel( + model=os.getenv("OPENAI_DEFAULT_MODEL", "gpt-4o-mini"), + openai_client=client, + ) + + +# Create agent +agent = Agent( + name="switchable-assistant", + model=create_model(), + instructions="""You are a helpful assistant. + Be concise, accurate, and friendly.""", +) + + +async def stream_response(user_message: str) -> AsyncIterator[str]: + """Stream agent response as SSE.""" + import json + + result = Runner.run_streamed(agent, user_message) + + async for event in result.stream_events(): + if hasattr(event, "data") and hasattr(event.data, "delta"): + chunk = event.data.delta + if chunk: + yield f"data: {json.dumps({'text': chunk})}\n\n" + + yield f"data: {json.dumps({'done': True})}\n\n" + + +@app.post("/chatkit/api") +async def chatkit_endpoint(request: Request): + event = await request.json() + user_message = event.get("message", {}).get("content", "") + + return StreamingResponse( + stream_response(user_message), + media_type="text/event-stream", + ) + + +@app.get("/health") +async def health(): + provider = os.getenv("LLM_PROVIDER", "openai") + return {"status": "healthy", "provider": provider} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +Usage: +```bash +# Run with Gemini +LLM_PROVIDER=gemini GEMINI_API_KEY=your-key uvicorn switchable_backend:app + +# Run with OpenAI +LLM_PROVIDER=openai OPENAI_API_KEY=your-key uvicorn switchable_backend:app +``` + +## Example 5: Frontend Configuration + +Next.js frontend configuration for Gemini backend. + +```tsx +// app/chat/page.tsx +"use client"; + +import { ChatKitWidget } from "@anthropic-ai/chatkit"; + +export default function ChatPage() { + return ( + { + const token = await getAuthToken(); // Your auth logic + + return fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${token}`, + }, + }); + }, + }, + // Widget configuration + theme: "light", + placeholder: "Ask me anything...", + }} + /> + ); +} +``` + +```tsx +// app/layout.tsx +// CRITICAL: Load CDN for widget styling + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {/* REQUIRED: ChatKit CDN for widget styling */} +