" in result.text
+
+ def test_task_to_input_no_custom_task(self, converter):
+ """Test that non-custom tasks return None."""
+ from datetime import datetime
+
+ from chatkit.types import TaskItem, ThoughtTask
+
+ task_item = TaskItem(
+ id="task_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="task",
+ task=ThoughtTask(type="thought", title="Think", content="Thinking..."),
+ )
+
+ result = converter.task_to_input(task_item)
+ assert result is None
+
+ def test_workflow_to_input(self, converter):
+ """Test converting WorkflowItem to ChatMessages."""
+ from datetime import datetime
+
+ from chatkit.types import CustomTask, Workflow, WorkflowItem
+
+ workflow_item = WorkflowItem(
+ id="wf_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="workflow",
+ workflow=Workflow(
+ type="custom",
+ tasks=[
+ CustomTask(type="custom", title="Step 1", content="First step"),
+ CustomTask(type="custom", title="Step 2", content="Second step"),
+ ],
+ ),
+ )
+
+ result = converter.workflow_to_input(workflow_item)
+ assert isinstance(result, list)
+ assert len(result) == 2
+ assert all(isinstance(msg, ChatMessage) for msg in result)
+ assert "Step 1: First step" in result[0].text
+ assert "Step 2: Second step" in result[1].text
+
+ def test_workflow_to_input_empty(self, converter):
+ """Test that workflows with no custom tasks return None."""
+ from datetime import datetime
+
+ from chatkit.types import Workflow, WorkflowItem
+
+ workflow_item = WorkflowItem(
+ id="wf_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="workflow",
+ workflow=Workflow(type="custom", tasks=[]),
+ )
+
+ result = converter.workflow_to_input(workflow_item)
+ assert result is None
+
+ def test_widget_to_input(self, converter):
+ """Test converting WidgetItem to ChatMessage."""
+ from datetime import datetime
+
+ from chatkit.types import WidgetItem
+ from chatkit.widgets import Card, Text
+
+ widget_item = WidgetItem(
+ id="widget_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="widget",
+ widget=Card(key="card1", children=[Text(value="Hello")]),
+ )
+
+ result = converter.widget_to_input(widget_item)
+ assert isinstance(result, ChatMessage)
+ assert result.role == Role.USER
+ assert "widget_1" in result.text
+ assert "graphical UI widget" in result.text
+
+
+class TestSimpleToAgentInput:
+ """Tests for simple_to_agent_input helper function."""
+
+ async def test_simple_to_agent_input_empty_list(self):
+ """Test simple conversion with empty list."""
+ result = await simple_to_agent_input([])
+ assert result == []
+
+ async def test_simple_to_agent_input_with_text(self):
+ """Test simple conversion with text content."""
+ from datetime import datetime
+
+ from chatkit.types import UserMessageItem
+
+ input_item = UserMessageItem(
+ id="msg_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="user_message",
+ content=[UserMessageTextContent(text="Test message")],
+ attachments=[],
+ inference_options={},
+ )
+
+ result = await simple_to_agent_input(input_item)
+
+ assert len(result) == 1
+ assert isinstance(result[0], ChatMessage)
+ assert result[0].role == Role.USER
+ assert result[0].text == "Test message"
diff --git a/python/packages/chatkit/tests/test_streaming.py b/python/packages/chatkit/tests/test_streaming.py
new file mode 100644
index 0000000000..2e5041613a
--- /dev/null
+++ b/python/packages/chatkit/tests/test_streaming.py
@@ -0,0 +1,142 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Tests for Agent Framework to ChatKit streaming utilities."""
+
+from unittest.mock import Mock
+
+from agent_framework import AgentRunResponseUpdate, Role, TextContent
+from chatkit.types import (
+ ThreadItemAddedEvent,
+ ThreadItemDoneEvent,
+ ThreadItemUpdated,
+)
+
+from agent_framework_chatkit import stream_agent_response
+
+
+class TestStreamAgentResponse:
+ """Tests for stream_agent_response function."""
+
+ async def test_stream_empty_response(self):
+ """Test streaming empty response."""
+
+ async def empty_stream():
+ return
+ yield # Make it a generator
+
+ events = []
+ async for event in stream_agent_response(empty_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ assert len(events) == 0
+
+ async def test_stream_single_text_update(self):
+ """Test streaming single text update."""
+
+ async def single_update_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Hello world")])
+
+ events = []
+ async for event in stream_agent_response(single_update_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have: item_added, item_updated (delta), item_done
+ assert len(events) == 3
+
+ # Check event types
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemUpdated)
+ assert isinstance(events[2], ThreadItemDoneEvent)
+
+ # Check delta event
+ assert events[1].update.delta == "Hello world"
+
+ # Check final message content
+ assert len(events[2].item.content) == 1
+ assert events[2].item.content[0].text == "Hello world"
+
+ async def test_stream_multiple_text_updates(self):
+ """Test streaming multiple text updates."""
+
+ async def multiple_updates_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Hello ")])
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="world!")])
+
+ events = []
+ async for event in stream_agent_response(multiple_updates_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have: item_added, item_updated (delta 1), item_updated (delta 2), item_done
+ assert len(events) == 4
+
+ # Check event types
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemUpdated)
+ assert isinstance(events[2], ThreadItemUpdated)
+ assert isinstance(events[3], ThreadItemDoneEvent)
+
+ # Check delta events
+ assert events[1].update.delta == "Hello "
+ assert events[2].update.delta == "world!"
+
+ # Check final accumulated text
+ final_message_event = events[-1]
+ assert isinstance(final_message_event, ThreadItemDoneEvent)
+ assert final_message_event.item.content[0].text == "Hello world!"
+
+ async def test_stream_with_custom_id_generator(self):
+ """Test streaming with custom ID generator."""
+
+ def custom_id_generator(item_type: str) -> str:
+ return f"custom_{item_type}_123"
+
+ async def single_update_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Test")])
+
+ events = []
+ async for event in stream_agent_response(
+ single_update_stream(), thread_id="test_thread", generate_id=custom_id_generator
+ ):
+ events.append(event)
+
+ # Check that custom IDs are used
+ message_added_event = events[0]
+ assert message_added_event.item.id == "custom_msg_123"
+
+ async def test_stream_empty_content_updates(self):
+ """Test streaming updates with empty content."""
+
+ async def empty_content_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[])
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=None)
+
+ events = []
+ async for event in stream_agent_response(empty_content_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have item_added and item_done
+ assert len(events) == 2
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemDoneEvent)
+
+ # Final message should have empty content
+ assert len(events[1].item.content) == 0
+
+ async def test_stream_non_text_content(self):
+ """Test streaming updates with non-text content."""
+ # Mock a content object without text attribute
+ non_text_content = Mock()
+ # Don't set text attribute
+ del non_text_content.text
+
+ async def non_text_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[non_text_content])
+
+ events = []
+ async for event in stream_agent_response(non_text_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have item_added and item_done, but no content since no text
+ assert len(events) == 2
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemDoneEvent)
diff --git a/python/packages/core/agent_framework/chatkit/__init__.py b/python/packages/core/agent_framework/chatkit/__init__.py
new file mode 100644
index 0000000000..163e6b412d
--- /dev/null
+++ b/python/packages/core/agent_framework/chatkit/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import importlib
+from typing import Any
+
+PACKAGE_NAME = "agent_framework_chatkit"
+PACKAGE_EXTRA = "chatkit"
+_IMPORTS = ["__version__", "ThreadItemConverter", "simple_to_agent_input", "stream_agent_response"]
+
+
+def __getattr__(name: str) -> Any:
+ if name in _IMPORTS:
+ try:
+ return getattr(importlib.import_module(PACKAGE_NAME), name)
+ except ModuleNotFoundError as exc:
+ raise ModuleNotFoundError(
+ f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
+ ) from exc
+ raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
+
+
+def __dir__() -> list[str]:
+ return _IMPORTS
diff --git a/python/packages/core/agent_framework/chatkit/__init__.pyi b/python/packages/core/agent_framework/chatkit/__init__.pyi
new file mode 100644
index 0000000000..9bd90e638d
--- /dev/null
+++ b/python/packages/core/agent_framework/chatkit/__init__.pyi
@@ -0,0 +1,10 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from agent_framework_chatkit import (
+ ThreadItemConverter,
+ __version__,
+ simple_to_agent_input,
+ stream_agent_response,
+)
+
+__all__ = ["ThreadItemConverter", "__version__", "simple_to_agent_input", "stream_agent_response"]
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 5b7d8fee8d..8db0916229 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -26,6 +26,7 @@ dependencies = [
"agent-framework-a2a",
"agent-framework-anthropic",
"agent-framework-azure-ai",
+ "agent-framework-chatkit",
"agent-framework-copilotstudio",
"agent-framework-devui",
"agent-framework-lab",
@@ -89,6 +90,7 @@ agent-framework = { workspace = true }
agent-framework-core = { workspace = true }
agent-framework-a2a = { workspace = true }
agent-framework-azure-ai = { workspace = true }
+agent-framework-chatkit = { workspace = true }
agent-framework-copilotstudio = { workspace = true }
agent-framework-lab = { workspace = true }
agent-framework-mem0 = { workspace = true }
@@ -240,6 +242,7 @@ pytest --import-mode=importlib
--cov=agent_framework
--cov=agent_framework_a2a
--cov=agent_framework_azure_ai
+--cov=agent_framework_chatkit
--cov=agent_framework_copilotstudio
--cov=agent_framework_mem0
--cov=agent_framework_redis
diff --git a/python/samples/demos/chatkit-integration/.gitignore b/python/samples/demos/chatkit-integration/.gitignore
new file mode 100644
index 0000000000..deb912b2f6
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/.gitignore
@@ -0,0 +1,4 @@
+*.db
+*.db-shm
+*.db-wal
+uploads/
\ No newline at end of file
diff --git a/python/samples/demos/chatkit-integration/README.md b/python/samples/demos/chatkit-integration/README.md
new file mode 100644
index 0000000000..28dfef398e
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/README.md
@@ -0,0 +1,268 @@
+# ChatKit Integration Sample with Weather Agent and Image Analysis
+
+This sample demonstrates how to integrate Microsoft Agent Framework with OpenAI ChatKit. It provides a complete implementation of a weather assistant with interactive widget visualization, image analysis, and file upload support.
+
+**Features:**
+
+- Weather information with interactive widgets
+- Image analysis using vision models
+- Current time queries
+- File upload with attachment storage
+- Chat interface with streaming responses
+- City selector widget with one-click weather
+
+## Architecture
+
+```mermaid
+graph TB
+ subgraph Frontend["React Frontend (ChatKit UI)"]
+ UI[ChatKit Components]
+ Upload[File Upload]
+ end
+
+ subgraph Backend["FastAPI Server"]
+ FastAPI[FastAPI Endpoints]
+
+ subgraph ChatKit["WeatherChatKitServer"]
+ Respond[respond method]
+ Action[action method]
+ end
+
+ subgraph Stores["Data & Storage Layer"]
+ SQLite[SQLiteStore
Store Protocol]
+ AttStore[FileBasedAttachmentStore
AttachmentStore Protocol]
+ DB[(SQLite DB
chatkit_demo.db)]
+ Files[/uploads directory/]
+ end
+
+ subgraph Integration["Agent Framework Integration"]
+ Converter[ThreadItemConverter]
+ Streamer[stream_agent_response]
+ Agent[ChatAgent]
+ end
+
+ Widgets[Widget Rendering
render_weather_widget
render_city_selector_widget]
+ end
+
+ subgraph Azure["Azure AI"]
+ Foundry[GPT-5
with Vision]
+ end
+
+ UI -->|HTTP POST /chatkit| FastAPI
+ Upload -->|HTTP POST /upload/id| FastAPI
+
+ FastAPI --> ChatKit
+
+ ChatKit -->|save/load threads| SQLite
+ ChatKit -->|save/load attachments| AttStore
+ ChatKit -->|convert messages| Converter
+
+ SQLite -.->|persist| DB
+ AttStore -.->|save files| Files
+ AttStore -.->|save metadata| SQLite
+
+ Converter -->|ChatMessage array| Agent
+ Agent -->|AgentRunResponseUpdate| Streamer
+ Streamer -->|ThreadStreamEvent| ChatKit
+
+ ChatKit --> Widgets
+ Widgets -->|WidgetItem| ChatKit
+
+ Agent <-->|Chat Completions API| Foundry
+
+ ChatKit -->|ThreadStreamEvent| FastAPI
+ FastAPI -->|SSE Stream| UI
+
+ style ChatKit fill:#e1f5ff
+ style Stores fill:#fff4e1
+ style Integration fill:#f0e1ff
+ style Azure fill:#e1ffe1
+```
+
+### Server Implementation
+
+The sample implements a ChatKit server using the `ChatKitServer` base class from the `chatkit` package:
+
+**Core Components:**
+
+- **`WeatherChatKitServer`**: Custom ChatKit server implementation that:
+
+ - Extends `ChatKitServer[dict[str, Any]]`
+ - Uses Agent Framework's `ChatAgent` with Azure OpenAI
+ - Converts ChatKit messages to Agent Framework format using `ThreadItemConverter`
+ - Streams responses back to ChatKit using `stream_agent_response`
+ - Creates and streams interactive widgets after agent responses
+
+- **`SQLiteStore`**: Data persistence layer that:
+
+ - Implements the `Store[dict[str, Any]]` protocol from ChatKit
+ - Persists threads, messages, and attachment metadata in SQLite
+ - Provides thread management and item history
+ - Stores attachment metadata for the upload lifecycle
+
+- **`FileBasedAttachmentStore`**: File storage implementation that:
+ - Implements the `AttachmentStore[dict[str, Any]]` protocol from ChatKit
+ - Stores uploaded files on the local filesystem (in `./uploads` directory)
+ - Generates upload URLs for two-phase file upload
+ - Saves attachment metadata to the data store for upload tracking
+ - Provides preview URLs for images
+
+**Key Integration Points:**
+
+```python
+# Converting ChatKit messages to Agent Framework
+converter = ThreadItemConverter(
+ attachment_data_fetcher=self._fetch_attachment_data
+)
+agent_messages = await converter.to_agent_input(user_message_item)
+
+# Running agent and streaming back to ChatKit
+async for event in stream_agent_response(
+ self.weather_agent.run_stream(agent_messages),
+ thread_id=thread.id,
+):
+ yield event
+
+# Streaming widgets
+widget = render_weather_widget(weather_data)
+async for event in stream_widget(thread_id=thread.id, widget=widget):
+ yield event
+```
+
+## Installation and Setup
+
+### Prerequisites
+
+- Python 3.10+
+- Node.js 18.18+ and npm 9+
+- Azure OpenAI service configured
+- Azure CLI for authentication (`az login`)
+
+### Backend Setup
+
+1. **Install Python packages:**
+
+```bash
+cd python/samples/demos/chatkit-integration
+pip install agent-framework-chatkit fastapi uvicorn azure-identity
+```
+
+2. **Configure Azure OpenAI:**
+
+```bash
+export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
+export AZURE_OPENAI_API_VERSION="2024-06-01"
+export AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="gpt-4o"
+```
+
+3. **Authenticate with Azure:**
+
+```bash
+az login
+```
+
+### Frontend Setup
+
+Install the Node.js dependencies:
+
+```bash
+cd frontend
+npm install
+```
+
+## How to Run
+
+### Start the Backend Server
+
+From the `chatkit-integration` directory:
+
+```bash
+python app.py
+```
+
+Or with auto-reload for development:
+
+```bash
+uvicorn app:app --host 127.0.0.1 --port 8001 --reload
+```
+
+The backend will start on `http://localhost:8001`
+
+### Start the Frontend Development Server
+
+In a new terminal, from the `frontend` directory:
+
+```bash
+npm run dev
+```
+
+The frontend will start on `http://localhost:5171`
+
+### Access the Application
+
+Open your browser and navigate to:
+
+```
+http://localhost:5171
+```
+
+You can now:
+
+- Ask about weather in any location (weather widgets display automatically)
+- Upload images for analysis using the attachment button
+- Get the current time
+- Ask to see available cities and click city buttons for instant weather
+
+### Project Structure
+
+```
+chatkit-integration/
+├── app.py # FastAPI backend with ChatKitServer implementation
+├── store.py # SQLiteStore implementation
+├── attachment_store.py # FileBasedAttachmentStore implementation
+├── weather_widget.py # Widget rendering functions
+├── chatkit_demo.db # SQLite database (auto-created)
+├── uploads/ # Uploaded files directory (auto-created)
+└── frontend/
+ ├── package.json
+ ├── vite.config.ts
+ ├── index.html
+ └── src/
+ ├── main.tsx
+ └── App.tsx # ChatKit UI integration
+```
+
+### Configuration
+
+You can customize the application by editing constants at the top of `app.py`:
+
+```python
+# Server configuration
+SERVER_HOST = "127.0.0.1" # Bind to localhost only for security (local dev)
+SERVER_PORT = 8001
+SERVER_BASE_URL = f"http://localhost:{SERVER_PORT}"
+
+# Database configuration
+DATABASE_PATH = "chatkit_demo.db"
+
+# File storage configuration
+UPLOADS_DIRECTORY = "./uploads"
+
+# User context
+DEFAULT_USER_ID = "demo_user"
+```
+
+### Sample Conversations
+
+Try these example queries:
+
+- "What's the weather like in Tokyo?"
+- "Show me available cities" (displays interactive city selector)
+- "What's the current time?"
+- Upload an image and ask "What do you see in this image?"
+
+## Learn More
+
+- [Agent Framework Documentation](https://aka.ms/agent-framework)
+- [ChatKit Documentation](https://platform.openai.com/docs/guides/chatkit)
+- [Azure OpenAI Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/)
diff --git a/python/samples/demos/chatkit-integration/__init__.py b/python/samples/demos/chatkit-integration/__init__.py
new file mode 100644
index 0000000000..2a50eae894
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/__init__.py
@@ -0,0 +1 @@
+# Copyright (c) Microsoft. All rights reserved.
diff --git a/python/samples/demos/chatkit-integration/app.py b/python/samples/demos/chatkit-integration/app.py
new file mode 100644
index 0000000000..ed5fd2dd6e
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/app.py
@@ -0,0 +1,538 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""
+ChatKit Integration Sample with Weather Agent and Image Analysis
+
+This sample demonstrates how to integrate Microsoft Agent Framework with OpenAI ChatKit
+using a weather tool with widget visualization, image analysis, and Azure OpenAI. It shows
+a complete ChatKit server implementation using Agent Framework agents with proper FastAPI
+setup, interactive weather widgets, and vision capabilities for analyzing uploaded images.
+"""
+
+import logging
+from collections.abc import AsyncIterator, Callable
+from datetime import datetime, timezone
+from random import randint
+from typing import Annotated, Any
+
+import uvicorn
+from azure.identity import AzureCliCredential
+from fastapi import FastAPI, File, Request, UploadFile
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import FileResponse, JSONResponse, Response, StreamingResponse
+from pydantic import Field
+
+# ============================================================================
+# Configuration Constants
+# ============================================================================
+
+# Server configuration
+SERVER_HOST = "127.0.0.1" # Bind to localhost only for security (local dev)
+SERVER_PORT = 8001
+SERVER_BASE_URL = f"http://localhost:{SERVER_PORT}"
+
+# Database configuration
+DATABASE_PATH = "chatkit_demo.db"
+
+# File storage configuration
+UPLOADS_DIRECTORY = "./uploads"
+
+# User context
+DEFAULT_USER_ID = "demo_user"
+
+# Logging configuration
+LOG_LEVEL = logging.INFO
+LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+# ============================================================================
+# Logging Setup
+# ============================================================================
+
+logging.basicConfig(
+ level=LOG_LEVEL,
+ format=LOG_FORMAT,
+ datefmt=LOG_DATE_FORMAT,
+)
+logger = logging.getLogger(__name__)
+
+# Agent Framework imports
+from agent_framework import AgentRunResponseUpdate, ChatAgent, ChatMessage, FunctionResultContent, Role
+from agent_framework.azure import AzureOpenAIChatClient
+
+# Agent Framework ChatKit integration
+from agent_framework_chatkit import ThreadItemConverter, stream_agent_response
+
+# Local imports
+from attachment_store import FileBasedAttachmentStore
+
+# ChatKit imports
+from chatkit.actions import Action
+from chatkit.server import ChatKitServer
+from chatkit.store import StoreItemType, default_generate_id
+from chatkit.types import (
+ ThreadItemDoneEvent,
+ ThreadMetadata,
+ ThreadStreamEvent,
+ UserMessageItem,
+ WidgetItem,
+)
+from chatkit.widgets import WidgetRoot
+from store import SQLiteStore
+from weather_widget import (
+ WeatherData,
+ city_selector_copy_text,
+ render_city_selector_widget,
+ render_weather_widget,
+ weather_widget_copy_text,
+)
+
+
+class WeatherResponse(str):
+ """A string response that also carries WeatherData for widget creation."""
+
+ def __new__(cls, text: str, weather_data: WeatherData):
+ instance = super().__new__(cls, text)
+ instance.weather_data = weather_data # type: ignore
+ return instance
+
+
+async def stream_widget(
+ thread_id: str,
+ widget: WidgetRoot,
+ copy_text: str | None = None,
+ generate_id: Callable[[StoreItemType], str] = default_generate_id,
+) -> AsyncIterator[ThreadStreamEvent]:
+ """Stream a ChatKit widget as a ThreadStreamEvent.
+
+ This helper function creates a ChatKit widget item and yields it as a
+ ThreadItemDoneEvent that can be consumed by the ChatKit UI.
+
+ Args:
+ thread_id: The ChatKit thread ID for the conversation.
+ widget: The ChatKit widget to display.
+ copy_text: Optional text representation of the widget for copy/paste.
+ generate_id: Optional function to generate IDs for ChatKit items.
+
+ Yields:
+ ThreadStreamEvent: ChatKit event containing the widget.
+ """
+ item_id = generate_id("message")
+
+ widget_item = WidgetItem(
+ id=item_id,
+ thread_id=thread_id,
+ created_at=datetime.now(),
+ widget=widget,
+ copy_text=copy_text,
+ )
+
+ yield ThreadItemDoneEvent(type="thread.item.done", item=widget_item)
+
+
+def get_weather(
+ location: Annotated[str, Field(description="The location to get the weather for.")],
+) -> str:
+ """Get the weather for a given location.
+
+ Returns a string description with embedded WeatherData for widget creation.
+ """
+ logger.info(f"Fetching weather for location: {location}")
+
+ conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy", "foggy"]
+ temperature = randint(-5, 35)
+ condition = conditions[randint(0, len(conditions) - 1)]
+
+ # Add some realistic details
+ humidity = randint(30, 90)
+ wind_speed = randint(5, 25)
+
+ weather_data = WeatherData(
+ location=location,
+ condition=condition,
+ temperature=temperature,
+ humidity=humidity,
+ wind_speed=wind_speed,
+ )
+
+ logger.debug(f"Weather data generated: {condition}, {temperature}°C, {humidity}% humidity, {wind_speed} km/h wind")
+
+ # Return a WeatherResponse that is both a string (for the LLM) and carries structured data
+ text = (
+ f"Weather in {location}:\n"
+ f"• Condition: {condition.title()}\n"
+ f"• Temperature: {temperature}°C\n"
+ f"• Humidity: {humidity}%\n"
+ f"• Wind: {wind_speed} km/h"
+ )
+ return WeatherResponse(text, weather_data)
+
+
+def get_time() -> str:
+ """Get the current UTC time."""
+ current_time = datetime.now(timezone.utc)
+ logger.info("Getting current UTC time")
+ return f"Current UTC time: {current_time.strftime('%Y-%m-%d %H:%M:%S')} UTC"
+
+
+def show_city_selector() -> str:
+ """Show an interactive city selector widget to the user.
+
+ This function triggers the display of a widget that allows users
+ to select from popular cities to get weather information.
+
+ Returns a special marker string that will be detected to show the widget.
+ """
+ logger.info("Activating city selector widget")
+ return "__SHOW_CITY_SELECTOR__"
+
+
+class WeatherChatKitServer(ChatKitServer[dict[str, Any]]):
+ """ChatKit server implementation using Agent Framework.
+
+ This server integrates Agent Framework agents with ChatKit's server protocol,
+ providing weather information with interactive widgets and time queries through Azure OpenAI.
+ """
+
+ def __init__(self, data_store: SQLiteStore, attachment_store: FileBasedAttachmentStore):
+ super().__init__(data_store, attachment_store)
+
+ logger.info("Initializing WeatherChatKitServer")
+
+ # Create Agent Framework agent with Azure OpenAI
+ # For authentication, run `az login` command in terminal
+ try:
+ self.weather_agent = ChatAgent(
+ chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
+ instructions=(
+ "You are a helpful weather assistant with image analysis capabilities. "
+ "You can provide weather information for any location, tell the current time, "
+ "and analyze images that users upload. Be friendly and informative in your responses.\n\n"
+ "If a user asks to see a list of cities or wants to choose from available cities, "
+ "use the show_city_selector tool to display an interactive city selector.\n\n"
+ "When users upload images, you will automatically receive them and can analyze their content. "
+ "Describe what you see in detail and be helpful in answering questions about the images."
+ ),
+ tools=[get_weather, get_time, show_city_selector],
+ )
+ logger.info("Weather agent initialized successfully with Azure OpenAI")
+ except Exception as e:
+ logger.error(f"Failed to initialize weather agent: {e}")
+ raise
+
+ # Create ThreadItemConverter with attachment data fetcher
+ self.converter = ThreadItemConverter(
+ attachment_data_fetcher=self._fetch_attachment_data,
+ )
+
+ logger.info("WeatherChatKitServer initialized")
+
+ async def _fetch_attachment_data(self, attachment_id: str) -> bytes:
+ """Fetch attachment binary data for the converter.
+
+ Args:
+ attachment_id: The ID of the attachment to fetch.
+
+ Returns:
+ The binary data of the attachment.
+ """
+ return await attachment_store.read_attachment_bytes(attachment_id)
+
+ async def respond(
+ self,
+ thread: ThreadMetadata,
+ input_user_message: UserMessageItem | None,
+ context: dict[str, Any],
+ ) -> AsyncIterator[ThreadStreamEvent]:
+ """Handle incoming user messages and generate responses.
+
+ This method converts ChatKit messages to Agent Framework format using ThreadItemConverter,
+ runs the agent, converts the response back to ChatKit events using stream_agent_response,
+ and creates interactive weather widgets when weather data is queried.
+ """
+ from agent_framework import FunctionResultContent
+
+ if input_user_message is None:
+ logger.debug("Received None user message, skipping")
+ return
+
+ logger.info(f"Processing message for thread: {thread.id}")
+
+ try:
+ # Track weather data and city selector flag for this request
+ weather_data: WeatherData | None = None
+ show_city_selector = False
+
+ # Convert ChatKit user message to Agent Framework ChatMessage using ThreadItemConverter
+ agent_messages = await self.converter.to_agent_input(input_user_message)
+
+ if not agent_messages:
+ logger.warning("No messages after conversion")
+ return
+
+ logger.info(f"Running agent with {len(agent_messages)} message(s)")
+
+ # Run the Agent Framework agent with streaming
+ agent_stream = self.weather_agent.run_stream(agent_messages)
+
+ # Create an intercepting stream that extracts function results while passing through updates
+ async def intercept_stream() -> AsyncIterator[AgentRunResponseUpdate]:
+ nonlocal weather_data, show_city_selector
+ async for update in agent_stream:
+ # Check for function results in the update
+ if update.contents:
+ for content in update.contents:
+ if isinstance(content, FunctionResultContent):
+ result = content.result
+
+ # Check if it's a WeatherResponse (string subclass with weather_data attribute)
+ if isinstance(result, str) and hasattr(result, "weather_data"):
+ extracted_data = getattr(result, "weather_data", None)
+ if isinstance(extracted_data, WeatherData):
+ weather_data = extracted_data
+ logger.info(f"Weather data extracted: {weather_data.location}")
+ # Check if it's the city selector marker
+ elif isinstance(result, str) and result == "__SHOW_CITY_SELECTOR__":
+ show_city_selector = True
+ logger.info("City selector flag detected")
+ yield update
+
+ # Stream updates as ChatKit events with interception
+ async for event in stream_agent_response(
+ intercept_stream(),
+ thread_id=thread.id,
+ ):
+ yield event
+
+ # If weather data was collected during the tool call, create a widget
+ if weather_data is not None and isinstance(weather_data, WeatherData):
+ logger.info(f"Creating weather widget for location: {weather_data.location}")
+ # Create weather widget
+ widget = render_weather_widget(weather_data)
+ copy_text = weather_widget_copy_text(weather_data)
+
+ # Stream the widget
+ async for widget_event in stream_widget(thread_id=thread.id, widget=widget, copy_text=copy_text):
+ yield widget_event
+ logger.debug("Weather widget streamed successfully")
+
+ # If city selector should be shown, create and stream that widget
+ if show_city_selector:
+ logger.info("Creating city selector widget")
+ # Create city selector widget
+ selector_widget = render_city_selector_widget()
+ selector_copy_text = city_selector_copy_text()
+
+ # Stream the widget
+ async for widget_event in stream_widget(
+ thread_id=thread.id, widget=selector_widget, copy_text=selector_copy_text
+ ):
+ yield widget_event
+ logger.debug("City selector widget streamed successfully")
+
+ logger.info(f"Completed processing message for thread: {thread.id}")
+
+ except Exception as e:
+ logger.error(f"Error processing message for thread {thread.id}: {e}", exc_info=True)
+
+ async def action(
+ self,
+ thread: ThreadMetadata,
+ action: Action[str, Any],
+ sender: WidgetItem | None,
+ context: dict[str, Any],
+ ) -> AsyncIterator[ThreadStreamEvent]:
+ """Handle widget actions from the frontend.
+
+ This method processes actions triggered by interactive widgets,
+ such as city selection from the city selector widget.
+ """
+
+ logger.info(f"Received action: {action.type} for thread: {thread.id}")
+
+ if action.type == "city_selected":
+ # Extract city information from the action payload
+ city_label = action.payload.get("city_label", "Unknown")
+
+ logger.info(f"City selected: {city_label}")
+ logger.debug(f"Action payload: {action.payload}")
+
+ # Track weather data for this request
+ weather_data: WeatherData | None = None
+
+ # Create an agent message asking about the weather
+ agent_messages = [ChatMessage(role=Role.USER, text=f"What's the weather in {city_label}?")]
+
+ logger.debug(f"Processing weather query: {agent_messages[0].text}")
+
+ # Run the Agent Framework agent with streaming
+ agent_stream = self.weather_agent.run_stream(agent_messages)
+
+ # Create an intercepting stream that extracts function results while passing through updates
+ async def intercept_stream() -> AsyncIterator[AgentRunResponseUpdate]:
+ nonlocal weather_data
+ async for update in agent_stream:
+ # Check for function results in the update
+ if update.contents:
+ for content in update.contents:
+ if isinstance(content, FunctionResultContent):
+ result = content.result
+
+ # Check if it's a WeatherResponse (string subclass with weather_data attribute)
+ if isinstance(result, str) and hasattr(result, "weather_data"):
+ extracted_data = getattr(result, "weather_data", None)
+ if isinstance(extracted_data, WeatherData):
+ weather_data = extracted_data
+ logger.info(f"Weather data extracted: {weather_data.location}")
+ yield update
+
+ # Stream updates as ChatKit events with interception
+ async for event in stream_agent_response(
+ intercept_stream(),
+ thread_id=thread.id,
+ ):
+ yield event
+
+ # If weather data was collected during the tool call, create a widget
+ if weather_data is not None and isinstance(weather_data, WeatherData):
+ logger.info(f"Creating weather widget for: {weather_data.location}")
+ # Create weather widget
+ widget = render_weather_widget(weather_data)
+ copy_text = weather_widget_copy_text(weather_data)
+
+ # Stream the widget
+ async for widget_event in stream_widget(thread_id=thread.id, widget=widget, copy_text=copy_text):
+ yield widget_event
+ logger.debug("Weather widget created successfully from action")
+ else:
+ logger.warning("No weather data available to create widget after action")
+
+
+# FastAPI application setup
+app = FastAPI(
+ title="ChatKit Weather & Vision Agent",
+ description="Weather and image analysis assistant powered by Agent Framework and Azure OpenAI",
+ version="1.0.0",
+)
+
+# Add CORS middleware to allow frontend connections
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # In production, specify exact origins
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Initialize data store and ChatKit server
+logger.info("Initializing application components")
+data_store = SQLiteStore(db_path=DATABASE_PATH)
+attachment_store = FileBasedAttachmentStore(
+ uploads_dir=UPLOADS_DIRECTORY,
+ base_url=SERVER_BASE_URL,
+ data_store=data_store,
+)
+chatkit_server = WeatherChatKitServer(data_store, attachment_store)
+logger.info("Application initialization complete")
+
+
+@app.post("/chatkit")
+async def chatkit_endpoint(request: Request):
+ """Main ChatKit endpoint that handles all ChatKit requests.
+
+ This endpoint follows the ChatKit server protocol and handles both
+ streaming and non-streaming responses.
+ """
+ logger.debug(f"Received ChatKit request from {request.client}")
+ request_body = await request.body()
+
+ # Create context following the working examples pattern
+ context = {"request": request}
+
+ try:
+ # Process the request using ChatKit server
+ result = await chatkit_server.process(request_body, context)
+
+ # Return appropriate response type
+ if hasattr(result, "__aiter__"): # StreamingResult
+ logger.debug("Returning streaming response")
+ return StreamingResponse(result, media_type="text/event-stream") # type: ignore[arg-type]
+ # NonStreamingResult
+ logger.debug("Returning non-streaming response")
+ return Response(content=result.json, media_type="application/json") # type: ignore[union-attr]
+ except Exception as e:
+ logger.error(f"Error processing ChatKit request: {e}", exc_info=True)
+ raise
+
+
+@app.post("/upload/{attachment_id}")
+async def upload_file(attachment_id: str, file: UploadFile = File(...)):
+ """Handle file upload for two-phase upload.
+
+ The client POSTs the file bytes here after creating the attachment
+ via the ChatKit attachments.create endpoint.
+ """
+ logger.info(f"Receiving file upload for attachment: {attachment_id}")
+
+ try:
+ # Read file contents
+ contents = await file.read()
+
+ # Save to disk
+ file_path = attachment_store.get_file_path(attachment_id)
+ file_path.write_bytes(contents)
+
+ logger.info(f"Saved {len(contents)} bytes to {file_path}")
+
+ # Load the attachment metadata from the data store
+ attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID})
+
+ # Clear the upload_url since upload is complete
+ attachment.upload_url = None
+
+ # Save the updated attachment back to the store
+ await data_store.save_attachment(attachment, {"user_id": DEFAULT_USER_ID})
+
+ # Return the attachment metadata as JSON
+ return JSONResponse(content=attachment.model_dump(mode="json"))
+
+ except Exception as e:
+ logger.error(f"Error uploading file for attachment {attachment_id}: {e}", exc_info=True)
+ return JSONResponse(status_code=500, content={"error": f"Failed to upload file: {str(e)}"})
+
+
+@app.get("/preview/{attachment_id}")
+async def preview_image(attachment_id: str):
+ """Serve image preview/thumbnail.
+
+ For simplicity, this serves the full image. In production, you should
+ generate and cache thumbnails.
+ """
+ logger.debug(f"Serving preview for attachment: {attachment_id}")
+
+ try:
+ file_path = attachment_store.get_file_path(attachment_id)
+
+ if not file_path.exists():
+ return JSONResponse(status_code=404, content={"error": "File not found"})
+
+ # Determine media type from file extension or attachment metadata
+ # For simplicity, we'll try to load from the store
+ try:
+ attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID})
+ media_type = attachment.mime_type
+ except Exception:
+ # Default to binary if we can't determine
+ media_type = "application/octet-stream"
+
+ return FileResponse(file_path, media_type=media_type)
+
+ except Exception as e:
+ logger.error(f"Error serving preview for attachment {attachment_id}: {e}", exc_info=True)
+ return JSONResponse(status_code=500, content={"error": str(e)})
+
+
+if __name__ == "__main__":
+ # Run the server
+ logger.info(f"Starting ChatKit Weather Agent server on {SERVER_HOST}:{SERVER_PORT}")
+ uvicorn.run(app, host=SERVER_HOST, port=SERVER_PORT, log_level="info")
diff --git a/python/samples/demos/chatkit-integration/attachment_store.py b/python/samples/demos/chatkit-integration/attachment_store.py
new file mode 100644
index 0000000000..263af20f46
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/attachment_store.py
@@ -0,0 +1,121 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""File-based AttachmentStore implementation for ChatKit.
+
+This module provides a simple AttachmentStore implementation that stores
+uploaded files on the local filesystem. In production, you should use
+cloud storage like S3, Azure Blob Storage, or Google Cloud Storage.
+"""
+
+from pathlib import Path
+from typing import Any, TYPE_CHECKING
+
+from chatkit.store import AttachmentStore
+from chatkit.types import Attachment, AttachmentCreateParams, FileAttachment, ImageAttachment
+from pydantic import AnyUrl
+
+if TYPE_CHECKING:
+ from store import SQLiteStore
+
+
+class FileBasedAttachmentStore(AttachmentStore[dict[str, Any]]):
+ """File-based AttachmentStore that stores files on local disk.
+
+ This implementation stores uploaded files in a local directory and provides
+ upload URLs that point to the FastAPI upload endpoint. It supports both
+ image and file attachments.
+
+ Features:
+ - Stores files in a local uploads directory
+ - Generates upload URLs for two-phase upload
+ - Generates preview URLs for images
+ - Proper cleanup on deletion
+
+ Note: This is for demonstration purposes. In production, use cloud storage
+ with signed URLs for better security and scalability.
+ """
+
+ def __init__(
+ self,
+ uploads_dir: str = "./uploads",
+ base_url: str = "http://localhost:8001",
+ data_store: "SQLiteStore | None" = None,
+ ):
+ """Initialize the file-based attachment store.
+
+ Args:
+ uploads_dir: Directory where uploaded files will be stored
+ base_url: Base URL for generating upload and preview URLs
+ data_store: Optional data store to persist attachment metadata
+ """
+ self.uploads_dir = Path(uploads_dir)
+ self.base_url = base_url.rstrip("/")
+ self.data_store = data_store
+
+ # Create uploads directory if it doesn't exist
+ self.uploads_dir.mkdir(parents=True, exist_ok=True)
+
+ def get_file_path(self, attachment_id: str) -> Path:
+ """Get the filesystem path for an attachment."""
+ return self.uploads_dir / attachment_id
+
+ async def delete_attachment(self, attachment_id: str, context: dict[str, Any]) -> None:
+ """Delete an attachment and its file from disk."""
+ file_path = self.get_file_path(attachment_id)
+ if file_path.exists():
+ file_path.unlink()
+
+ async def create_attachment(
+ self, input: AttachmentCreateParams, context: dict[str, Any]
+ ) -> Attachment:
+ """Create an attachment with upload URL for two-phase upload.
+
+ This creates the attachment metadata and returns upload URLs that
+ the client will use to POST the actual file bytes.
+ """
+ # Generate unique ID for this attachment
+ attachment_id = self.generate_attachment_id(input.mime_type, context)
+
+ # Generate upload URL that points to our FastAPI upload endpoint
+ upload_url = f"{self.base_url}/upload/{attachment_id}"
+
+ # Create appropriate attachment type based on MIME type
+ if input.mime_type.startswith("image/"):
+ # For images, also provide a preview URL
+ preview_url = f"{self.base_url}/preview/{attachment_id}"
+
+ attachment = ImageAttachment(
+ id=attachment_id,
+ type="image",
+ mime_type=input.mime_type,
+ name=input.name,
+ upload_url=AnyUrl(upload_url),
+ preview_url=AnyUrl(preview_url),
+ )
+ else:
+ # For files, just provide upload URL
+ attachment = FileAttachment(
+ id=attachment_id,
+ type="file",
+ mime_type=input.mime_type,
+ name=input.name,
+ upload_url=AnyUrl(upload_url),
+ )
+
+ # Save attachment metadata to data store so it's available during upload
+ if self.data_store is not None:
+ await self.data_store.save_attachment(attachment, context)
+
+ return attachment
+
+ async def read_attachment_bytes(self, attachment_id: str) -> bytes:
+ """Read the raw bytes of an uploaded attachment.
+
+ This is used by the ThreadItemConverter to create base64-encoded
+ content for sending to the Agent Framework.
+ """
+ file_path = self.get_file_path(attachment_id)
+ if not file_path.exists():
+ raise FileNotFoundError(f"Attachment {attachment_id} not found on disk")
+
+ return file_path.read_bytes()
diff --git a/python/samples/demos/chatkit-integration/frontend/index.html b/python/samples/demos/chatkit-integration/frontend/index.html
new file mode 100644
index 0000000000..82837ef519
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ ChatKit + Agent Framework Demo
+
+
+
+
+
+
+
+
+
diff --git a/python/samples/demos/chatkit-integration/frontend/package-lock.json b/python/samples/demos/chatkit-integration/frontend/package-lock.json
new file mode 100644
index 0000000000..9cf6bb6b86
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/package-lock.json
@@ -0,0 +1,1437 @@
+{
+ "name": "chatkit-agent-framework-demo",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "chatkit-agent-framework-demo",
+ "version": "0.1.0",
+ "dependencies": {
+ "@openai/chatkit-react": "^0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "typescript": "^5.4.0",
+ "vite": "^7.1.9"
+ },
+ "engines": {
+ "node": ">=18.18",
+ "npm": ">=9"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
+ "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
+ "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
+ "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
+ "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
+ "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
+ "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
+ "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
+ "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
+ "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
+ "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
+ "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
+ "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
+ "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
+ "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
+ "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
+ "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
+ "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
+ "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
+ "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
+ "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
+ "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@openai/chatkit": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/@openai/chatkit/-/chatkit-0.0.0.tgz",
+ "integrity": "sha512-9YomebDd2dpWFR3s1fiEtNknXmEC8QYt//2ConGjr/4geWdRqunEpO+i7yJXYEGLJbkmB4lxwKmbwWJA4pvpSg==",
+ "license": "MIT"
+ },
+ "node_modules/@openai/chatkit-react": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/@openai/chatkit-react/-/chatkit-react-0.0.0.tgz",
+ "integrity": "sha512-ppoAKiWKUJGIlKuFQ0mgPRVMAAjJ+PonAzdo1p7BQmTEZtwFI8vq6W7ZRN2UTfzZZIKbJ2diwU6ePbYSKsePuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@openai/chatkit": "0.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
+ "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
+ "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
+ "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
+ "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
+ "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
+ "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
+ "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
+ "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
+ "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
+ "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
+ "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
+ "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
+ "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
+ "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
+ "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
+ "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
+ "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
+ "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
+ "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
+ "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
+ "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
+ "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
+ "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.24"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.13.5",
+ "@swc/core-darwin-x64": "1.13.5",
+ "@swc/core-linux-arm-gnueabihf": "1.13.5",
+ "@swc/core-linux-arm64-gnu": "1.13.5",
+ "@swc/core-linux-arm64-musl": "1.13.5",
+ "@swc/core-linux-x64-gnu": "1.13.5",
+ "@swc/core-linux-x64-musl": "1.13.5",
+ "@swc/core-win32-arm64-msvc": "1.13.5",
+ "@swc/core-win32-ia32-msvc": "1.13.5",
+ "@swc/core-win32-x64-msvc": "1.13.5"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz",
+ "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz",
+ "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz",
+ "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz",
+ "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz",
+ "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz",
+ "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz",
+ "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz",
+ "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz",
+ "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz",
+ "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.25",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
+ "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.2",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
+ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
+ "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@swc/core": "^1.12.11"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
+ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.10",
+ "@esbuild/android-arm": "0.25.10",
+ "@esbuild/android-arm64": "0.25.10",
+ "@esbuild/android-x64": "0.25.10",
+ "@esbuild/darwin-arm64": "0.25.10",
+ "@esbuild/darwin-x64": "0.25.10",
+ "@esbuild/freebsd-arm64": "0.25.10",
+ "@esbuild/freebsd-x64": "0.25.10",
+ "@esbuild/linux-arm": "0.25.10",
+ "@esbuild/linux-arm64": "0.25.10",
+ "@esbuild/linux-ia32": "0.25.10",
+ "@esbuild/linux-loong64": "0.25.10",
+ "@esbuild/linux-mips64el": "0.25.10",
+ "@esbuild/linux-ppc64": "0.25.10",
+ "@esbuild/linux-riscv64": "0.25.10",
+ "@esbuild/linux-s390x": "0.25.10",
+ "@esbuild/linux-x64": "0.25.10",
+ "@esbuild/netbsd-arm64": "0.25.10",
+ "@esbuild/netbsd-x64": "0.25.10",
+ "@esbuild/openbsd-arm64": "0.25.10",
+ "@esbuild/openbsd-x64": "0.25.10",
+ "@esbuild/openharmony-arm64": "0.25.10",
+ "@esbuild/sunos-x64": "0.25.10",
+ "@esbuild/win32-arm64": "0.25.10",
+ "@esbuild/win32-ia32": "0.25.10",
+ "@esbuild/win32-x64": "0.25.10"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
+ "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.52.4",
+ "@rollup/rollup-android-arm64": "4.52.4",
+ "@rollup/rollup-darwin-arm64": "4.52.4",
+ "@rollup/rollup-darwin-x64": "4.52.4",
+ "@rollup/rollup-freebsd-arm64": "4.52.4",
+ "@rollup/rollup-freebsd-x64": "4.52.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.4",
+ "@rollup/rollup-linux-arm64-musl": "4.52.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.4",
+ "@rollup/rollup-linux-x64-gnu": "4.52.4",
+ "@rollup/rollup-linux-x64-musl": "4.52.4",
+ "@rollup/rollup-openharmony-arm64": "4.52.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.4",
+ "@rollup/rollup-win32-x64-gnu": "4.52.4",
+ "@rollup/rollup-win32-x64-msvc": "4.52.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
+ "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ }
+ }
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/package.json b/python/samples/demos/chatkit-integration/frontend/package.json
new file mode 100644
index 0000000000..65d65d1d53
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "chatkit-agent-framework-demo",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "engines": {
+ "node": ">=18.18",
+ "npm": ">=9"
+ },
+ "dependencies": {
+ "@openai/chatkit-react": "^0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "typescript": "^5.4.0",
+ "vite": "^7.1.9"
+ }
+}
\ No newline at end of file
diff --git a/python/samples/demos/chatkit-integration/frontend/src/App.tsx b/python/samples/demos/chatkit-integration/frontend/src/App.tsx
new file mode 100644
index 0000000000..13f42d17c9
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/src/App.tsx
@@ -0,0 +1,33 @@
+import { ChatKit, useChatKit } from "@openai/chatkit-react";
+
+const CHATKIT_API_URL = "/chatkit";
+const CHATKIT_API_DOMAIN_KEY =
+ import.meta.env.VITE_CHATKIT_API_DOMAIN_KEY ?? "domain_pk_localhost_dev";
+
+export default function App() {
+ const chatkit = useChatKit({
+ api: {
+ url: CHATKIT_API_URL,
+ domainKey: CHATKIT_API_DOMAIN_KEY,
+ uploadStrategy: { type: "two_phase" },
+ },
+ startScreen: {
+ greeting: "Hello! I'm your weather and image analysis assistant. Ask me about the weather in any location or upload images for me to analyze.",
+ prompts: [
+ { label: "Weather in New York", prompt: "What's the weather in New York?" },
+ { label: "Select City to Get Weather", prompt: "Show me the city selector for weather" },
+ { label: "Current Time", prompt: "What time is it?" },
+ { label: "Analyze an Image", prompt: "I'll upload an image for you to analyze" },
+ ],
+ },
+ composer: {
+ placeholder: "Ask about weather or upload an image...",
+ attachments: {
+ enabled: true,
+ accept: { "image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"] },
+ },
+ },
+ });
+
+ return ;
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/src/main.tsx b/python/samples/demos/chatkit-integration/frontend/src/main.tsx
new file mode 100644
index 0000000000..0937a0fa0f
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/src/main.tsx
@@ -0,0 +1,15 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+
+const container = document.getElementById("root");
+
+if (!container) {
+ throw new Error("Root element with id 'root' not found");
+}
+
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/python/samples/demos/chatkit-integration/frontend/src/vite-env.d.ts b/python/samples/demos/chatkit-integration/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000000..11f02fe2a0
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/python/samples/demos/chatkit-integration/frontend/tsconfig.json b/python/samples/demos/chatkit-integration/frontend/tsconfig.json
new file mode 100644
index 0000000000..3934b8f6d6
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/tsconfig.node.json b/python/samples/demos/chatkit-integration/frontend/tsconfig.node.json
new file mode 100644
index 0000000000..42872c59f5
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/vite.config.ts b/python/samples/demos/chatkit-integration/frontend/vite.config.ts
new file mode 100644
index 0000000000..ebf0200e51
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+
+const backendTarget = process.env.BACKEND_URL ?? "http://127.0.0.1:8001";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: "0.0.0.0",
+ port: 5171,
+ proxy: {
+ "/chatkit": {
+ target: backendTarget,
+ changeOrigin: true,
+ },
+ },
+ // For production deployments, you need to add your public domains to this list
+ allowedHosts: [
+ // You can remove these examples added just to demonstrate how to configure the allowlist
+ ".ngrok.io",
+ ".trycloudflare.com",
+ ],
+ },
+});
diff --git a/python/samples/demos/chatkit-integration/store.py b/python/samples/demos/chatkit-integration/store.py
new file mode 100644
index 0000000000..17fb746bed
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/store.py
@@ -0,0 +1,361 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""SQLite-based store implementation for ChatKit data persistence.
+
+This module provides a complete Store implementation using SQLite for data persistence.
+It includes proper thread safety, user isolation, and follows the ChatKit Store protocol.
+"""
+
+import sqlite3
+import uuid
+from typing import Any
+
+from chatkit.store import Store, NotFoundError
+from chatkit.types import (
+ Attachment,
+ Page,
+ ThreadItem,
+ ThreadMetadata,
+)
+from pydantic import BaseModel
+
+
+class ThreadData(BaseModel):
+ """Model for serializing thread data to SQLite."""
+ thread: ThreadMetadata
+
+
+class ItemData(BaseModel):
+ """Model for serializing thread item data to SQLite."""
+ item: ThreadItem
+
+
+class AttachmentData(BaseModel):
+ """Model for serializing attachment data to SQLite."""
+ attachment: Attachment
+
+
+class SQLiteStore(Store[dict[str, Any]]):
+ """SQLite-based store implementation for ChatKit data.
+
+ This implementation follows the pattern from the ChatKit Python tests
+ and provides persistent storage for threads, messages, and attachments.
+
+ Features:
+ - Thread-safe SQLite connections with WAL mode
+ - User isolation for multi-tenant support
+ - Proper error handling and transaction management
+ - Complete Store protocol implementation
+
+ Note: This is for demonstration purposes. In production, you should
+ implement proper error handling, connection pooling, and migration strategies.
+ """
+
+ def __init__(self, db_path: str | None = None):
+ self.db_path = db_path or "chatkit_demo.db" # Use file-based DB for demo
+ self._create_tables()
+
+ def _create_connection(self):
+ # Enable thread safety and WAL mode for better concurrent access
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
+ conn.execute("PRAGMA journal_mode=WAL")
+ return conn
+
+ def _create_tables(self):
+ with self._create_connection() as conn:
+ # Create threads table
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS threads (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ data TEXT NOT NULL
+ )"""
+ )
+
+ # Create items table
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS items (
+ id TEXT PRIMARY KEY,
+ thread_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ data TEXT NOT NULL
+ )"""
+ )
+
+ # Create attachments table
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS attachments (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ data TEXT NOT NULL
+ )"""
+ )
+ conn.commit()
+
+ def generate_thread_id(self, context: dict[str, Any]) -> str:
+ return f"thr_{uuid.uuid4().hex[:8]}"
+
+ def generate_item_id(
+ self,
+ item_type: str,
+ thread: ThreadMetadata,
+ context: dict[str, Any],
+ ) -> str:
+ prefix_map = {
+ "message": "msg",
+ "tool_call": "tc",
+ "task": "tsk",
+ "workflow": "wf",
+ "attachment": "atc",
+ }
+ prefix = prefix_map.get(item_type, "itm")
+ return f"{prefix}_{uuid.uuid4().hex[:8]}"
+
+ async def load_thread(self, thread_id: str, context: dict[str, Any]) -> ThreadMetadata:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ cursor = conn.execute(
+ "SELECT data FROM threads WHERE id = ? AND user_id = ?",
+ (thread_id, user_id),
+ ).fetchone()
+
+ if cursor is None:
+ raise NotFoundError(f"Thread {thread_id} not found")
+
+ thread_data = ThreadData.model_validate_json(cursor[0])
+ return thread_data.thread
+
+ async def save_thread(self, thread: ThreadMetadata, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ thread_data = ThreadData(thread=thread)
+
+ # Replace existing thread data
+ conn.execute(
+ "DELETE FROM threads WHERE id = ? AND user_id = ?",
+ (thread.id, user_id),
+ )
+ conn.execute(
+ "INSERT INTO threads (id, user_id, created_at, data) VALUES (?, ?, ?, ?)",
+ (
+ thread.id,
+ user_id,
+ thread.created_at.isoformat(),
+ thread_data.model_dump_json(),
+ ),
+ )
+ conn.commit()
+
+ async def load_thread_items(
+ self,
+ thread_id: str,
+ after: str | None,
+ limit: int,
+ order: str,
+ context: dict[str, Any],
+ ) -> Page[ThreadItem]:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ created_after: str | None = None
+ if after:
+ after_cursor = conn.execute(
+ "SELECT created_at FROM items WHERE id = ? AND user_id = ?",
+ (after, user_id),
+ ).fetchone()
+ if after_cursor is None:
+ raise NotFoundError(f"Item {after} not found")
+ created_after = after_cursor[0]
+
+ query = """
+ SELECT data FROM items
+ WHERE thread_id = ? AND user_id = ?
+ """
+ params: list[Any] = [thread_id, user_id]
+
+ if created_after:
+ query += " AND created_at > ?" if order == "asc" else " AND created_at < ?"
+ params.append(created_after)
+
+ query += f" ORDER BY created_at {order} LIMIT ?"
+ params.append(limit + 1)
+
+ items_cursor = conn.execute(query, params).fetchall()
+ items = [
+ ItemData.model_validate_json(row[0]).item for row in items_cursor
+ ]
+
+ has_more = len(items) > limit
+ if has_more:
+ items = items[:limit]
+
+ return Page[ThreadItem](
+ data=items,
+ has_more=has_more,
+ after=items[-1].id if items else None
+ )
+
+ async def save_attachment(self, attachment: Attachment, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ attachment_data = AttachmentData(attachment=attachment)
+ conn.execute(
+ "INSERT OR REPLACE INTO attachments (id, user_id, data) VALUES (?, ?, ?)",
+ (
+ attachment.id,
+ user_id,
+ attachment_data.model_dump_json(),
+ ),
+ )
+ conn.commit()
+
+ async def load_attachment(self, attachment_id: str, context: dict[str, Any]) -> Attachment:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ cursor = conn.execute(
+ "SELECT data FROM attachments WHERE id = ? AND user_id = ?",
+ (attachment_id, user_id),
+ ).fetchone()
+
+ if cursor is None:
+ raise NotFoundError(f"Attachment {attachment_id} not found")
+
+ attachment_data = AttachmentData.model_validate_json(cursor[0])
+ return attachment_data.attachment
+
+ async def delete_attachment(self, attachment_id: str, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ conn.execute(
+ "DELETE FROM attachments WHERE id = ? AND user_id = ?",
+ (attachment_id, user_id),
+ )
+ conn.commit()
+
+ async def load_threads(
+ self,
+ limit: int,
+ after: str | None,
+ order: str,
+ context: dict[str, Any],
+ ) -> Page[ThreadMetadata]:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ created_after: str | None = None
+ if after:
+ after_cursor = conn.execute(
+ "SELECT created_at FROM threads WHERE id = ? AND user_id = ?",
+ (after, user_id),
+ ).fetchone()
+ if after_cursor is None:
+ raise NotFoundError(f"Thread {after} not found")
+ created_after = after_cursor[0]
+
+ query = "SELECT data FROM threads WHERE user_id = ?"
+ params: list[Any] = [user_id]
+
+ if created_after:
+ query += " AND created_at > ?" if order == "asc" else " AND created_at < ?"
+ params.append(created_after)
+
+ query += f" ORDER BY created_at {order} LIMIT ?"
+ params.append(limit + 1)
+
+ threads_cursor = conn.execute(query, params).fetchall()
+ threads = [
+ ThreadData.model_validate_json(row[0]).thread for row in threads_cursor
+ ]
+
+ has_more = len(threads) > limit
+ if has_more:
+ threads = threads[:limit]
+
+ return Page[ThreadMetadata](
+ data=threads,
+ has_more=has_more,
+ after=threads[-1].id if threads else None
+ )
+
+ async def add_thread_item(
+ self, thread_id: str, item: ThreadItem, context: dict[str, Any]
+ ) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ item_data = ItemData(item=item)
+ conn.execute(
+ "INSERT INTO items (id, thread_id, user_id, created_at, data) VALUES (?, ?, ?, ?, ?)",
+ (
+ item.id,
+ thread_id,
+ user_id,
+ item.created_at.isoformat(),
+ item_data.model_dump_json(),
+ ),
+ )
+ conn.commit()
+
+ async def save_item(self, thread_id: str, item: ThreadItem, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ item_data = ItemData(item=item)
+ conn.execute(
+ "UPDATE items SET data = ? WHERE id = ? AND thread_id = ? AND user_id = ?",
+ (
+ item_data.model_dump_json(),
+ item.id,
+ thread_id,
+ user_id,
+ ),
+ )
+ conn.commit()
+
+ async def load_item(self, thread_id: str, item_id: str, context: dict[str, Any]) -> ThreadItem:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ cursor = conn.execute(
+ "SELECT data FROM items WHERE id = ? AND thread_id = ? AND user_id = ?",
+ (item_id, thread_id, user_id),
+ ).fetchone()
+
+ if cursor is None:
+ raise NotFoundError(f"Item {item_id} not found in thread {thread_id}")
+
+ item_data = ItemData.model_validate_json(cursor[0])
+ return item_data.item
+
+ async def delete_thread(self, thread_id: str, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ conn.execute(
+ "DELETE FROM threads WHERE id = ? AND user_id = ?",
+ (thread_id, user_id),
+ )
+ conn.execute(
+ "DELETE FROM items WHERE thread_id = ? AND user_id = ?",
+ (thread_id, user_id),
+ )
+ conn.commit()
+
+ async def delete_thread_item(
+ self, thread_id: str, item_id: str, context: dict[str, Any]
+ ) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ conn.execute(
+ "DELETE FROM items WHERE id = ? AND thread_id = ? AND user_id = ?",
+ (item_id, thread_id, user_id),
+ )
+ conn.commit()
diff --git a/python/samples/demos/chatkit-integration/weather_widget.py b/python/samples/demos/chatkit-integration/weather_widget.py
new file mode 100644
index 0000000000..834f7a031d
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/weather_widget.py
@@ -0,0 +1,437 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Weather widget rendering for ChatKit integration sample."""
+
+import base64
+from dataclasses import dataclass
+
+from chatkit.actions import ActionConfig
+from chatkit.widgets import Box, Button, Card, Col, Image, Row, Text, Title, WidgetRoot
+
+WEATHER_ICON_COLOR = "#1D4ED8"
+WEATHER_ICON_ACCENT = "#DBEAFE"
+
+# Popular cities for the selector
+POPULAR_CITIES = [
+ {"value": "seattle", "label": "Seattle, WA", "description": "Pacific Northwest"},
+ {"value": "new_york", "label": "New York, NY", "description": "East Coast"},
+ {"value": "san_francisco", "label": "San Francisco, CA", "description": "Bay Area"},
+ {"value": "chicago", "label": "Chicago, IL", "description": "Midwest"},
+ {"value": "miami", "label": "Miami, FL", "description": "Southeast"},
+ {"value": "austin", "label": "Austin, TX", "description": "Southwest"},
+ {"value": "boston", "label": "Boston, MA", "description": "New England"},
+ {"value": "denver", "label": "Denver, CO", "description": "Mountain West"},
+ {"value": "portland", "label": "Portland, OR", "description": "Pacific Northwest"},
+ {"value": "atlanta", "label": "Atlanta, GA", "description": "Southeast"},
+]
+
+# Mapping from city values to display names for weather queries
+CITY_VALUE_TO_NAME = {city["value"]: city["label"] for city in POPULAR_CITIES}
+
+
+
+def _sun_svg() -> str:
+ """Generate SVG for sunny weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _cloud_svg() -> str:
+ """Generate SVG for cloudy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _rain_svg() -> str:
+ """Generate SVG for rainy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _storm_svg() -> str:
+ """Generate SVG for stormy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _snow_svg() -> str:
+ """Generate SVG for snowy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _fog_svg() -> str:
+ """Generate SVG for foggy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _encode_svg(svg: str) -> str:
+ """Encode SVG as base64 data URI."""
+ encoded = base64.b64encode(svg.encode("utf-8")).decode("ascii")
+ return f"data:image/svg+xml;base64,{encoded}"
+
+
+# Weather condition to icon mapping
+WEATHER_ICONS = {
+ "sunny": _encode_svg(_sun_svg()),
+ "cloudy": _encode_svg(_cloud_svg()),
+ "rainy": _encode_svg(_rain_svg()),
+ "stormy": _encode_svg(_storm_svg()),
+ "snowy": _encode_svg(_snow_svg()),
+ "foggy": _encode_svg(_fog_svg()),
+}
+
+DEFAULT_WEATHER_ICON = _encode_svg(_cloud_svg())
+
+
+@dataclass
+class WeatherData:
+ """Weather data container."""
+
+ location: str
+ condition: str
+ temperature: int
+ humidity: int
+ wind_speed: int
+
+
+def render_weather_widget(data: WeatherData) -> WidgetRoot:
+ """Render a weather widget from weather data.
+
+ Args:
+ data: WeatherData containing weather information
+
+ Returns:
+ A ChatKit WidgetRoot (Card) displaying the weather information
+ """
+ # Get weather icon
+ weather_icon_src = WEATHER_ICONS.get(data.condition.lower(), DEFAULT_WEATHER_ICON)
+
+ # Build the widget
+ header = Box(
+ padding=5,
+ background="surface-tertiary",
+ children=[
+ Row(
+ justify="between",
+ align="center",
+ children=[
+ Col(
+ align="start",
+ gap=1,
+ children=[
+ Text(
+ value=data.location,
+ size="lg",
+ weight="semibold",
+ ),
+ Text(
+ value="Current conditions",
+ color="tertiary",
+ size="xs",
+ ),
+ ],
+ ),
+ Box(
+ padding=3,
+ radius="full",
+ background="blue-100",
+ children=[
+ Image(
+ src=weather_icon_src,
+ alt=data.condition,
+ size=28,
+ fit="contain",
+ )
+ ],
+ ),
+ ],
+ ),
+ Row(
+ align="start",
+ gap=4,
+ children=[
+ Title(
+ value=f"{data.temperature}°C",
+ size="lg",
+ weight="semibold",
+ ),
+ Col(
+ align="start",
+ gap=1,
+ children=[
+ Text(
+ value=data.condition.title(),
+ color="secondary",
+ size="sm",
+ weight="medium",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ )
+
+ # Details section
+ details = Box(
+ padding=5,
+ gap=4,
+ children=[
+ Text(value="Weather details", weight="semibold", size="sm"),
+ Row(
+ gap=3,
+ wrap="wrap",
+ children=[
+ _detail_chip("Humidity", f"{data.humidity}%"),
+ _detail_chip("Wind", f"{data.wind_speed} km/h"),
+ ],
+ ),
+ ],
+ )
+
+ return Card(
+ key="weather",
+ padding=0,
+ children=[header, details],
+ )
+
+
+def _detail_chip(label: str, value: str) -> Box:
+ """Create a detail chip widget component."""
+ return Box(
+ padding=3,
+ radius="xl",
+ background="surface-tertiary",
+ width=150,
+ minWidth=150,
+ maxWidth=150,
+ minHeight=80,
+ maxHeight=80,
+ flex="0 0 auto",
+ children=[
+ Col(
+ align="stretch",
+ gap=2,
+ children=[
+ Text(value=label, size="xs", weight="medium", color="tertiary"),
+ Row(
+ justify="center",
+ margin={"top": 2},
+ children=[Text(value=value, weight="semibold", size="lg")],
+ ),
+ ],
+ )
+ ],
+ )
+
+
+def weather_widget_copy_text(data: WeatherData) -> str:
+ """Generate plain text representation of weather data.
+
+ Args:
+ data: WeatherData containing weather information
+
+ Returns:
+ Plain text description for copy/paste functionality
+ """
+ return (
+ f"Weather in {data.location}:\n"
+ f"• Condition: {data.condition.title()}\n"
+ f"• Temperature: {data.temperature}°C\n"
+ f"• Humidity: {data.humidity}%\n"
+ f"• Wind: {data.wind_speed} km/h"
+ )
+
+
+def render_city_selector_widget() -> WidgetRoot:
+ """Render an interactive city selector widget.
+
+ This widget displays popular cities as a visual selection interface.
+ Users can click or ask about any city to get weather information.
+
+ Returns:
+ A ChatKit WidgetRoot (Card) with city selection display
+ """
+ # Create location icon SVG
+ location_icon = _encode_svg(
+ '"
+ )
+
+ # Header section
+ header = Box(
+ padding=5,
+ background="surface-tertiary",
+ children=[
+ Row(
+ gap=3,
+ align="center",
+ children=[
+ Box(
+ padding=3,
+ radius="full",
+ background="blue-100",
+ children=[
+ Image(
+ src=location_icon,
+ alt="Location",
+ size=28,
+ fit="contain",
+ )
+ ],
+ ),
+ Col(
+ align="start",
+ gap=1,
+ children=[
+ Title(
+ value="Popular Cities",
+ size="md",
+ weight="semibold",
+ ),
+ Text(
+ value="Select a city or ask about any location",
+ color="tertiary",
+ size="xs",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ )
+
+ # Create city chips in a grid layout
+ city_chips: list[Button] = []
+ for city in POPULAR_CITIES:
+ # Create a button that sends an action to query weather for the selected city
+ chip = Button(
+ label=city["label"],
+ variant="outline",
+ size="md",
+ onClickAction=ActionConfig(
+ type="city_selected",
+ payload={"city_value": city["value"], "city_label": city["label"]},
+ handler="server", # Handle on server-side
+ ),
+ )
+ city_chips.append(chip)
+
+ # Arrange in rows of 3
+ city_rows: list[Row] = []
+ for i in range(0, len(city_chips), 3):
+ row_chips: list[Button] = city_chips[i : i + 3]
+ city_rows.append(
+ Row(
+ gap=3,
+ wrap="wrap",
+ justify="start",
+ children=list(row_chips), # Convert to generic list
+ )
+ )
+
+ # Cities display section
+ cities_section = Box(
+ padding=5,
+ gap=3,
+ children=[
+ *city_rows,
+ Box(
+ padding=3,
+ radius="md",
+ background="blue-50",
+ children=[
+ Text(
+ value="💡 Click any city to get its weather, or ask about any other location!",
+ size="xs",
+ color="secondary",
+ ),
+ ],
+ ),
+ ],
+ )
+
+ return Card(
+ key="city_selector",
+ padding=0,
+ children=[header, cities_section],
+ )
+
+
+def city_selector_copy_text() -> str:
+ """Generate plain text representation of city selector.
+
+ Returns:
+ Plain text description for copy/paste functionality
+ """
+ cities_list = "\n".join([f"• {city['label']}" for city in POPULAR_CITIES])
+ return f"Popular cities (click to get weather):\n{cities_list}\n\nYou can also ask about weather in any other location!"
diff --git a/python/uv.lock b/python/uv.lock
index fdcc316836..7e9e6798a7 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -33,6 +33,7 @@ members = [
"agent-framework-a2a",
"agent-framework-anthropic",
"agent-framework-azure-ai",
+ "agent-framework-chatkit",
"agent-framework-copilotstudio",
"agent-framework-core",
"agent-framework-devui",
@@ -79,6 +80,7 @@ dependencies = [
{ name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-anthropic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-azure-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "agent-framework-chatkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-devui", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -121,6 +123,7 @@ requires-dist = [
{ name = "agent-framework-a2a", editable = "packages/a2a" },
{ name = "agent-framework-anthropic", editable = "packages/anthropic" },
{ name = "agent-framework-azure-ai", editable = "packages/azure-ai" },
+ { name = "agent-framework-chatkit", editable = "packages/chatkit" },
{ name = "agent-framework-copilotstudio", editable = "packages/copilotstudio" },
{ name = "agent-framework-core", editable = "packages/core" },
{ name = "agent-framework-devui", editable = "packages/devui" },
@@ -205,6 +208,21 @@ requires-dist = [
{ name = "azure-ai-projects", specifier = ">=1.0.0b11" },
]
+[[package]]
+name = "agent-framework-chatkit"
+version = "1.0.0b251001"
+source = { editable = "packages/chatkit" }
+dependencies = [
+ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "openai-chatkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "agent-framework-core", editable = "packages/core" },
+ { name = "openai-chatkit", specifier = ">=1.1.0,<2.0.0" },
+]
+
[[package]]
name = "agent-framework-copilotstudio"
version = "1.0.0b251104"
@@ -2086,6 +2104,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
]
+[[package]]
+name = "griffe"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" },
+]
+
[[package]]
name = "grpcio"
version = "1.76.0"
@@ -3497,6 +3527,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" },
]
+[[package]]
+name = "openai-agents"
+version = "0.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a4/37/2b4f828840d3ff32d82b813c3371ec9ee26b3b8dc6b4acbb7a4a579f617a/openai_agents-0.3.3.tar.gz", hash = "sha256:b016381a6890e1cb6879eb23c53c35f8c2312be1117f1cd4e4b5e2463150839f", size = 1816230, upload-time = "2025-09-30T23:20:24.22Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/59/fd49fd2c3184c0d5fedb8c9c456ae9852154828bca7ee69dce004ea83188/openai_agents-0.3.3-py3-none-any.whl", hash = "sha256:aa2c74e010b923c09f166e63a51fae8c850c62df8581b84bafcbe5bd208d1505", size = 210893, upload-time = "2025-09-30T23:20:22.037Z" },
+]
+
+[[package]]
+name = "openai-chatkit"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "openai-agents", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/19/9948f2996c224aff01f6ef415784042c3d710c1e950937b16d9a2c07e47e/openai_chatkit-1.1.0.tar.gz", hash = "sha256:5594341aab29b56fd3396e8d3ad1962ebdb8c44f062a8e315663ac8cf1371c6b", size = 49480, upload-time = "2025-11-03T22:50:05.089Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/82/07db74ee63d54f3cadab3baaa1534bef0d3699a94d2618c76050cccb0cfe/openai_chatkit-1.1.0-py3-none-any.whl", hash = "sha256:e78f021899fbef1323f3adc3a686f9fe5ee184cd997799a917e9013833e760ba", size = 35424, upload-time = "2025-11-03T22:50:03.788Z" },
+]
+
[[package]]
name = "opentelemetry-api"
version = "1.38.0"
@@ -5919,6 +5982,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" },
]
+[[package]]
+name = "types-requests"
+version = "2.32.4.20250913"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"