openai/chatkit-python
Publicmirrored fromhttps://github.com/openai/chatkit-pythonAvailable
docs/quickstart.md
261lines · modecode
| 1 | # Quick start |
| 2 | |
| 3 | To get a basic ChatKit app running—a React chat UI talking to a Python server—clone and run the starter app: |
| 4 | |
| 5 | |
| 6 | ```sh |
| 7 | git clone https://github.com/openai/openai-chatkit-starter-app.git |
| 8 | cd openai-chatkit-starter-app/chatkit |
| 9 | npm install |
| 10 | npm run dev |
| 11 | ``` |
| 12 | |
| 13 | The sections below explain the core components and steps behind the starter app. |
| 14 | |
| 15 | ## Render chat UI |
| 16 | |
| 17 | !!! note "Quick start uses React" |
| 18 | This section shows the React integration using `@openai/chatkit-react`. |
| 19 | If you’re not using React, you can render ChatKit directly with vanilla JavaScript using `@openai/chatkit`. See the [ChatKit.js quick start](https://openai.github.io/chatkit-js/quickstart/) for details. |
| 20 | |
| 21 | Install the React bindings: |
| 22 | |
| 23 | ```sh |
| 24 | npm install @openai/chatkit-react |
| 25 | ``` |
| 26 | |
| 27 | In your index.html, load ChatKit.js: |
| 28 | |
| 29 | ```html |
| 30 | <!doctype html> |
| 31 | <html lang="en"> |
| 32 | <head> |
| 33 | <meta charset="UTF-8" /> |
| 34 | <script src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"></script> |
| 35 | </head> |
| 36 | <body> |
| 37 | <div id="root"></div> |
| 38 | </body> |
| 39 | </html> |
| 40 | ``` |
| 41 | |
| 42 | Wire up a minimal React app. Point `api.url` at your ChatKit server endpoint and pass the domain key you configured there. |
| 43 | |
| 44 | ```tsx |
| 45 | import {ChatKit, useChatKit} from "@openai/chatkit-react"; |
| 46 | |
| 47 | export function App() { |
| 48 | const chatkit = useChatKit({ |
| 49 | api: { |
| 50 | url: "http://localhost:8000/chatkit", |
| 51 | domainKey: "local-dev", // domain keys are optional in dev |
| 52 | }, |
| 53 | }); |
| 54 | |
| 55 | return <ChatKit control={chatkit.control} />; |
| 56 | } |
| 57 | ``` |
| 58 | |
| 59 | The chat UI will render, but sending messages will fail until you start the server below and provide a store for threads and messages. |
| 60 | |
| 61 | ## Run your ChatKit server |
| 62 | |
| 63 | Install the ChatKit Python package and expose a single `/chatkit` endpoint that forwards requests to a `ChatKitServer` instance. |
| 64 | |
| 65 | ```sh |
| 66 | pip install openai-chatkit |
| 67 | ``` |
| 68 | |
| 69 | Create `main.py` with a minimal server that is hard-coded to always reply “Hello, world!”—you'll replace this with an actual call to a model in [Respond to a user message](guides/respond-to-user-message.md). |
| 70 | |
| 71 | ```python |
| 72 | # Other imports omitted for brevity; see the starter repo for a runnable file with all imports. |
| 73 | from chatkit.server import ChatKitServer |
| 74 | |
| 75 | app = FastAPI() |
| 76 | |
| 77 | |
| 78 | class MyChatKitServer(ChatKitServer[dict]): |
| 79 | async def respond( |
| 80 | self, |
| 81 | thread: ThreadMetadata, |
| 82 | input_user_message: UserMessageItem | None, |
| 83 | context: dict, |
| 84 | ) -> AsyncIterator[ThreadStreamEvent]: |
| 85 | # Streams a fixed "Hello, world!" assistant message |
| 86 | yield ThreadItemDoneEvent( |
| 87 | item=AssistantMessageItem( |
| 88 | thread_id=thread.id, |
| 89 | id=self.store.generate_item_id("message", thread, context), |
| 90 | created_at=datetime.now(), |
| 91 | content=[AssistantMessageContent(text="Hello, world!")], |
| 92 | ), |
| 93 | ) |
| 94 | |
| 95 | # Create your server by passing a store implementation. |
| 96 | # MyChatKitStore is defined in the next section. |
| 97 | server = MyChatKitServer(store=MyChatKitStore()) |
| 98 | |
| 99 | |
| 100 | @app.post("/chatkit") |
| 101 | async def chatkit(request: Request): |
| 102 | result = await server.process(await request.body(), context={}) |
| 103 | if isinstance(result, StreamingResult): |
| 104 | return StreamingResponse(result, media_type="text/event-stream") |
| 105 | return Response(content=result.json, media_type="application/json") |
| 106 | ``` |
| 107 | |
| 108 | All ChatKit requests go to this single endpoint. Set `api.url` on the React side to match (`/chatkit` here), and `ChatKitServer` routes each request internally. |
| 109 | |
| 110 | |
| 111 | ## Store chat data |
| 112 | |
| 113 | ChatKit servers require a store to load and save threads, messages, and other items. |
| 114 | |
| 115 | For this quickstart, use a small in-memory store so conversations persist while the process is running, without introducing a database. This keeps the example minimal while still matching real ChatKit behavior. |
| 116 | |
| 117 | |
| 118 | ```python |
| 119 | from collections import defaultdict |
| 120 | from chatkit.store import NotFoundError, Store |
| 121 | from chatkit.types import Attachment, Page, ThreadItem, ThreadMetadata |
| 122 | |
| 123 | |
| 124 | class MyChatKitStore(Store[dict]): |
| 125 | def __init__(self): |
| 126 | self.threads: dict[str, ThreadMetadata] = {} |
| 127 | self.items: dict[str, list[ThreadItem]] = defaultdict(list) |
| 128 | |
| 129 | async def load_thread(self, thread_id: str, context: dict) -> ThreadMetadata: |
| 130 | if thread_id not in self.threads: |
| 131 | raise NotFoundError(f"Thread {thread_id} not found") |
| 132 | return self.threads[thread_id] |
| 133 | |
| 134 | async def save_thread(self, thread: ThreadMetadata, context: dict) -> None: |
| 135 | self.threads[thread.id] = thread |
| 136 | |
| 137 | async def load_threads( |
| 138 | self, limit: int, after: str | None, order: str, context: dict |
| 139 | ) -> Page[ThreadMetadata]: |
| 140 | threads = list(self.threads.values()) |
| 141 | return self._paginate( |
| 142 | threads, after, limit, order, sort_key=lambda t: t.created_at, cursor_key=lambda t: t.id |
| 143 | ) |
| 144 | |
| 145 | async def load_thread_items( |
| 146 | self, thread_id: str, after: str | None, limit: int, order: str, context: dict |
| 147 | ) -> Page[ThreadItem]: |
| 148 | items = self.items.get(thread_id, []) |
| 149 | return self._paginate( |
| 150 | items, after, limit, order, sort_key=lambda i: i.created_at, cursor_key=lambda i: i.id |
| 151 | ) |
| 152 | |
| 153 | async def add_thread_item( |
| 154 | self, thread_id: str, item: ThreadItem, context: dict |
| 155 | ) -> None: |
| 156 | self.items[thread_id].append(item) |
| 157 | |
| 158 | async def save_item( |
| 159 | self, thread_id: str, item: ThreadItem, context: dict |
| 160 | ) -> None: |
| 161 | items = self.items[thread_id] |
| 162 | for idx, existing in enumerate(items): |
| 163 | if existing.id == item.id: |
| 164 | items[idx] = item |
| 165 | return |
| 166 | items.append(item) |
| 167 | |
| 168 | async def load_item( |
| 169 | self, thread_id: str, item_id: str, context: dict |
| 170 | ) -> ThreadItem: |
| 171 | for item in self.items.get(thread_id, []): |
| 172 | if item.id == item_id: |
| 173 | return item |
| 174 | raise NotFoundError(f"Item {item_id} not found in thread {thread_id}") |
| 175 | |
| 176 | async def delete_thread(self, thread_id: str, context: dict) -> None: |
| 177 | self.threads.pop(thread_id, None) |
| 178 | self.items.pop(thread_id, None) |
| 179 | |
| 180 | async def delete_thread_item( |
| 181 | self, thread_id: str, item_id: str, context: dict |
| 182 | ) -> None: |
| 183 | self.items[thread_id] = [ |
| 184 | item for item in self.items.get(thread_id, []) if item.id != item_id |
| 185 | ] |
| 186 | |
| 187 | def _paginate(self, rows: list, after: str | None, limit: int, order: str, sort_key, cursor_key): |
| 188 | sorted_rows = sorted(rows, key=sort_key, reverse=order == "desc") |
| 189 | start = 0 |
| 190 | if after: |
| 191 | for idx, row in enumerate(sorted_rows): |
| 192 | if cursor_key(row) == after: |
| 193 | start = idx + 1 |
| 194 | break |
| 195 | data = sorted_rows[start : start + limit] |
| 196 | has_more = start + limit < len(sorted_rows) |
| 197 | next_after = cursor_key(data[-1]) if has_more and data else None |
| 198 | return Page(data=data, has_more=has_more, after=next_after) |
| 199 | |
| 200 | # Attachments are intentionally not implemented for the quickstart |
| 201 | |
| 202 | async def save_attachment( |
| 203 | self, attachment: Attachment, context: dict |
| 204 | ) -> None: |
| 205 | raise NotImplementedError() |
| 206 | |
| 207 | async def load_attachment( |
| 208 | self, attachment_id: str, context: dict |
| 209 | ) -> Attachment: |
| 210 | raise NotImplementedError() |
| 211 | |
| 212 | async def delete_attachment(self, attachment_id: str, context: dict) -> None: |
| 213 | raise NotImplementedError() |
| 214 | |
| 215 | ``` |
| 216 | |
| 217 | This store implements only the methods required for basic chat while the server is running; persistence across restarts and attachments are intentionally omitted. |
| 218 | |
| 219 | For production, replace this with a database-backed store (for example, Postgres or MySQL) so threads and items persist across restarts. |
| 220 | |
| 221 | |
| 222 | ## Generate model responses |
| 223 | |
| 224 | Replace the hardcoded "Hello, World!" reply from [Run your ChatKit server](#run-your-chatkit-server) with an Agents SDK call to generate real responses. Set `OPENAI_API_KEY` in your environment before running. |
| 225 | |
| 226 | Use ChatKit's Agents SDK helpers to simplify request conversion and streaming. The `simple_to_agent_input` helper translates ChatKit thread items to agent input items, and `stream_agent_response` turns the streamed run into ChatKit events: |
| 227 | |
| 228 | |
| 229 | ```python |
| 230 | from agents import Agent, Runner |
| 231 | from chatkit.agents import AgentContext, simple_to_agent_input, stream_agent_response |
| 232 | |
| 233 | assistant = Agent( |
| 234 | name="assistant", |
| 235 | instructions="You are a helpful assistant.", |
| 236 | model="gpt-4.1-mini", |
| 237 | ) |
| 238 | |
| 239 | class MyChatKitServer(ChatKitServer[dict]): |
| 240 | async def respond( |
| 241 | self, |
| 242 | thread: ThreadMetadata, |
| 243 | input_user_message: UserMessageItem | None, |
| 244 | context: dict, |
| 245 | ) -> AsyncIterator[ThreadStreamEvent]: |
| 246 | # Convert recent thread items (which includes the user message) to model input |
| 247 | items_page = await self.store.load_thread_items( |
| 248 | thread.id, |
| 249 | after=None, |
| 250 | limit=20, |
| 251 | order="asc", |
| 252 | context=context, |
| 253 | ) |
| 254 | input_items = await simple_to_agent_input(items_page.data) |
| 255 | |
| 256 | # Stream the run through ChatKit events |
| 257 | agent_context = AgentContext(thread=thread, store=self.store, request_context=context) |
| 258 | result = Runner.run_streamed(assistant, input_items, context=agent_context) |
| 259 | async for event in stream_agent_response(agent_context, result): |
| 260 | yield event |
| 261 | ``` |
| 262 | |