diff --git a/chatkit/server.py b/chatkit/server.py index fa405a2..5963ff8 100644 --- a/chatkit/server.py +++ b/chatkit/server.py @@ -276,6 +276,16 @@ def respond( input_user_message: UserMessageItem | None, context: TContext, ) -> AsyncIterator[ThreadStreamEvent]: + """Stream `ThreadStreamEvent` instances for a new user message. + + Args: + thread: Metadata for the thread being processed. + input_user_message: The incoming message the server should respond to, if any. + context: Arbitrary per-request context provided by the caller. + + Returns: + An async iterator that yields events representing the server's response. + """ pass async def add_feedback( # noqa: B027 diff --git a/chatkit/types.py b/chatkit/types.py index 832b1c3..c81dc2a 100644 --- a/chatkit/types.py +++ b/chatkit/types.py @@ -15,6 +15,8 @@ class Page(BaseModel, Generic[T]): + """Paginated collection of records returned from the API.""" + data: list[T] = [] has_more: bool = False after: str | None = None @@ -24,117 +26,163 @@ class Page(BaseModel, Generic[T]): class BaseReq(BaseModel): + """Base class for all request payloads.""" + metadata: dict[str, Any] = Field(default_factory=dict) """Arbitrary integration-specific metadata.""" class ThreadsGetByIdReq(BaseReq): + """Request to fetch a single thread by its identifier.""" + type: Literal["threads.get_by_id"] = "threads.get_by_id" params: ThreadGetByIdParams class ThreadGetByIdParams(BaseModel): + """Parameters for retrieving a thread by id.""" + thread_id: str class ThreadsCreateReq(BaseReq): + """Request to create a new thread from a user message.""" + type: Literal["threads.create"] = "threads.create" params: ThreadCreateParams class ThreadCreateParams(BaseModel): + """User input required to create a thread.""" + input: UserMessageInput class ThreadListParams(BaseModel): + """Pagination parameters for listing threads.""" + limit: int | None = None order: Literal["asc", "desc"] = "desc" after: str | None = None class ThreadsListReq(BaseReq): + """Request to list threads.""" + type: Literal["threads.list"] = "threads.list" params: ThreadListParams class ThreadsAddUserMessageReq(BaseReq): + """Request to append a user message to a thread.""" + type: Literal["threads.add_user_message"] = "threads.add_user_message" params: ThreadAddUserMessageParams class ThreadAddUserMessageParams(BaseModel): + """Parameters for adding a user message to a thread.""" + input: UserMessageInput thread_id: str class ThreadsAddClientToolOutputReq(BaseReq): + """Request to add a client tool's output to a thread.""" + type: Literal["threads.add_client_tool_output"] = "threads.add_client_tool_output" params: ThreadAddClientToolOutputParams class ThreadAddClientToolOutputParams(BaseModel): + """Parameters for recording tool output in a thread.""" + thread_id: str result: Any class ThreadsCustomActionReq(BaseReq): + """Request to execute a custom action within a thread.""" + type: Literal["threads.custom_action"] = "threads.custom_action" params: ThreadCustomActionParams class ThreadCustomActionParams(BaseModel): + """Parameters describing the custom action to execute.""" + thread_id: str item_id: str | None = None action: Action[str, Any] class ThreadsRetryAfterItemReq(BaseReq): + """Request to retry processing after a specific thread item.""" + type: Literal["threads.retry_after_item"] = "threads.retry_after_item" params: ThreadRetryAfterItemParams class ThreadRetryAfterItemParams(BaseModel): + """Parameters specifying which item to retry.""" + thread_id: str item_id: str class ItemsFeedbackReq(BaseReq): + """Request to submit feedback on specific items.""" + type: Literal["items.feedback"] = "items.feedback" params: ItemFeedbackParams class ItemFeedbackParams(BaseModel): + """Parameters describing feedback targets and sentiment.""" + thread_id: str item_ids: list[str] kind: FeedbackKind class AttachmentsDeleteReq(BaseReq): + """Request to remove an attachment.""" + type: Literal["attachments.delete"] = "attachments.delete" params: AttachmentDeleteParams class AttachmentDeleteParams(BaseModel): + """Parameters identifying an attachment to delete.""" + attachment_id: str class AttachmentsCreateReq(BaseReq): + """Request to register a new attachment.""" + type: Literal["attachments.create"] = "attachments.create" params: AttachmentCreateParams class AttachmentCreateParams(BaseModel): + """Metadata needed to initialize an attachment.""" + name: str size: int mime_type: str class ItemsListReq(BaseReq): + """Request to list items inside a thread.""" + type: Literal["items.list"] = "items.list" params: ItemsListParams class ItemsListParams(BaseModel): + """Pagination parameters for listing thread items.""" + thread_id: str limit: int | None = None order: Literal["asc", "desc"] = "desc" @@ -142,21 +190,29 @@ class ItemsListParams(BaseModel): class ThreadsUpdateReq(BaseReq): + """Request to update thread metadata.""" + type: Literal["threads.update"] = "threads.update" params: ThreadUpdateParams class ThreadUpdateParams(BaseModel): + """Parameters for updating a thread's properties.""" + thread_id: str title: str class ThreadsDeleteReq(BaseReq): + """Request to delete a thread.""" + type: Literal["threads.delete"] = "threads.delete" params: ThreadDeleteParams class ThreadDeleteParams(BaseModel): + """Parameters identifying a thread to delete.""" + thread_id: str @@ -167,6 +223,8 @@ class ThreadDeleteParams(BaseModel): | ThreadsRetryAfterItemReq | ThreadsCustomActionReq ) +"""Union of request types that produce streaming responses.""" + NonStreamingReq = ( ThreadsGetByIdReq @@ -178,6 +236,8 @@ class ThreadDeleteParams(BaseModel): | ThreadsUpdateReq | ThreadsDeleteReq ) +"""Union of request types that yield immediate responses.""" + ChatKitReq = Annotated[ StreamingReq | NonStreamingReq, @@ -186,6 +246,7 @@ class ThreadDeleteParams(BaseModel): def is_streaming_req(request: ChatKitReq) -> TypeIs[StreamingReq]: + """Return True if the given request should be processed as streaming.""" return isinstance( request, ( @@ -202,48 +263,66 @@ def is_streaming_req(request: ChatKitReq) -> TypeIs[StreamingReq]: class ThreadCreatedEvent(BaseModel): + """Event emitted when a thread is created.""" + type: Literal["thread.created"] = "thread.created" thread: Thread class ThreadUpdatedEvent(BaseModel): + """Event emitted when a thread is updated.""" + type: Literal["thread.updated"] = "thread.updated" thread: Thread class ThreadItemAddedEvent(BaseModel): + """Event emitted when a new item is added to a thread.""" + type: Literal["thread.item.added"] = "thread.item.added" item: ThreadItem class ThreadItemUpdated(BaseModel): + """Event describing an update to an existing thread item.""" + type: Literal["thread.item.updated"] = "thread.item.updated" item_id: str update: ThreadItemUpdate class ThreadItemDoneEvent(BaseModel): + """Event emitted when a thread item is marked complete.""" + type: Literal["thread.item.done"] = "thread.item.done" item: ThreadItem class ThreadItemRemovedEvent(BaseModel): + """Event emitted when a thread item is removed.""" + type: Literal["thread.item.removed"] = "thread.item.removed" item_id: str class ThreadItemReplacedEvent(BaseModel): + """Event emitted when a thread item is replaced.""" + type: Literal["thread.item.replaced"] = "thread.item.replaced" item: ThreadItem class ProgressUpdateEvent(BaseModel): + """Event providing incremental progress from the assistant.""" + type: Literal["progress_update"] = "progress_update" icon: IconName | None = None text: str class ErrorEvent(BaseModel): + """Event indicating an error occurred while processing a thread.""" + type: Literal["error"] = "error" code: ErrorCode | Literal["custom"] = Field(default="custom") message: str | None = None @@ -251,6 +330,8 @@ class ErrorEvent(BaseModel): class NoticeEvent(BaseModel): + """Event conveying a user-facing notice.""" + type: Literal["notice"] = "notice" level: Literal["info", "warning", "danger"] message: str @@ -273,11 +354,14 @@ class NoticeEvent(BaseModel): | NoticeEvent, Field(discriminator="type"), ] +"""Union of all streaming events emitted to clients.""" ### THREAD ITEM UPDATE TYPES class AssistantMessageContentPartAdded(BaseModel): + """Event emitted when new assistant content is appended.""" + type: Literal["assistant_message.content_part.added"] = ( "assistant_message.content_part.added" ) @@ -286,6 +370,8 @@ class AssistantMessageContentPartAdded(BaseModel): class AssistantMessageContentPartTextDelta(BaseModel): + """Event carrying incremental assistant text output.""" + type: Literal["assistant_message.content_part.text_delta"] = ( "assistant_message.content_part.text_delta" ) @@ -294,6 +380,8 @@ class AssistantMessageContentPartTextDelta(BaseModel): class AssistantMessageContentPartAnnotationAdded(BaseModel): + """Event announcing a new annotation on assistant content.""" + type: Literal["assistant_message.content_part.annotation_added"] = ( "assistant_message.content_part.annotation_added" ) @@ -303,6 +391,8 @@ class AssistantMessageContentPartAnnotationAdded(BaseModel): class AssistantMessageContentPartDone(BaseModel): + """Event indicating an assistant content part is finalized.""" + type: Literal["assistant_message.content_part.done"] = ( "assistant_message.content_part.done" ) @@ -311,6 +401,8 @@ class AssistantMessageContentPartDone(BaseModel): class WidgetStreamingTextValueDelta(BaseModel): + """Event streaming widget text deltas.""" + type: Literal["widget.streaming_text.value_delta"] = ( "widget.streaming_text.value_delta" ) @@ -320,23 +412,31 @@ class WidgetStreamingTextValueDelta(BaseModel): class WidgetRootUpdated(BaseModel): + """Event published when the widget root changes.""" + type: Literal["widget.root.updated"] = "widget.root.updated" widget: WidgetRoot class WidgetComponentUpdated(BaseModel): + """Event emitted when a widget component updates.""" + type: Literal["widget.component.updated"] = "widget.component.updated" component_id: str component: WidgetComponent class WorkflowTaskAdded(BaseModel): + """Event emitted when a workflow task is added.""" + type: Literal["workflow.task.added"] = "workflow.task.added" task_index: int task: Task class WorkflowTaskUpdated(BaseModel): + """Event emitted when a workflow task is updated.""" + type: Literal["workflow.task.updated"] = "workflow.task.updated" task_index: int task: Task @@ -353,12 +453,15 @@ class WorkflowTaskUpdated(BaseModel): | WorkflowTaskAdded | WorkflowTaskUpdated ) +"""Union of possible updates applied to thread items.""" ### THREAD TYPES class ThreadMetadata(BaseModel): + """Metadata describing a thread without its items.""" + title: str | None = None id: str created_at: datetime @@ -368,15 +471,21 @@ class ThreadMetadata(BaseModel): class ActiveStatus(BaseModel): + """Status indicating the thread is active.""" + type: Literal["active"] = Field(default="active", frozen=True) class LockedStatus(BaseModel): + """Status indicating the thread is locked.""" + type: Literal["locked"] = Field(default="locked", frozen=True) reason: str | None = None class ClosedStatus(BaseModel): + """Status indicating the thread is closed.""" + type: Literal["closed"] = Field(default="closed", frozen=True) reason: str | None = None @@ -385,9 +494,12 @@ class ClosedStatus(BaseModel): ActiveStatus | LockedStatus | ClosedStatus, Field(discriminator="type"), ] +"""Union of lifecycle states for a thread.""" class Thread(ThreadMetadata): + """Thread with its paginated items.""" + items: Page[ThreadItem] @@ -395,12 +507,16 @@ class Thread(ThreadMetadata): class ThreadItemBase(BaseModel): + """Base fields shared by all thread items.""" + id: str thread_id: str created_at: datetime class UserMessageItem(ThreadItemBase): + """Thread item representing a user message.""" + type: Literal["user_message"] = "user_message" content: list[UserMessageContent] attachments: list[Attachment] = Field(default_factory=list) @@ -409,11 +525,15 @@ class UserMessageItem(ThreadItemBase): class AssistantMessageItem(ThreadItemBase): + """Thread item representing an assistant message.""" + type: Literal["assistant_message"] = "assistant_message" content: list[AssistantMessageContent] class ClientToolCallItem(ThreadItemBase): + """Thread item capturing a client tool call.""" + type: Literal["client_tool_call"] = "client_tool_call" status: Literal["pending", "completed"] = "pending" call_id: str @@ -423,22 +543,30 @@ class ClientToolCallItem(ThreadItemBase): class WidgetItem(ThreadItemBase): + """Thread item containing widget content.""" + type: Literal["widget"] = "widget" widget: WidgetRoot copy_text: str | None = None class TaskItem(ThreadItemBase): + """Thread item containing a task.""" + type: Literal["task"] = "task" task: Task class WorkflowItem(ThreadItemBase): + """Thread item representing a workflow.""" + type: Literal["workflow"] = "workflow" workflow: Workflow class EndOfTurnItem(ThreadItemBase): + """Marker item indicating the assistant ends its turn.""" + type: Literal["end_of_turn"] = "end_of_turn" @@ -460,18 +588,23 @@ class HiddenContextItem(ThreadItemBase): | EndOfTurnItem, Field(discriminator="type"), ] +"""Union of all thread item variants.""" ### ASSISTANT MESSAGE TYPES class AssistantMessageContent(BaseModel): + """Assistant message content consisting of text and annotations.""" + annotations: list[Annotation] = Field(default_factory=list) text: str type: Literal["output_text"] = "output_text" class Annotation(BaseModel): + """Reference to supporting context attached to assistant output.""" + type: Literal["annotation"] = "annotation" source: URLSource | FileSource | EntitySource index: int | None = None @@ -481,6 +614,8 @@ class Annotation(BaseModel): class UserMessageInput(BaseModel): + """Payload describing a user message submission.""" + content: list[UserMessageContent] attachments: list[str] quoted_text: str | None = None @@ -488,11 +623,15 @@ class UserMessageInput(BaseModel): class UserMessageTextContent(BaseModel): + """User message content containing plaintext.""" + type: Literal["input_text"] = "input_text" text: str class UserMessageTagContent(BaseModel): + """User message content representing an interactive tag.""" + type: Literal["input_tag"] = "input_tag" id: str text: str @@ -503,18 +642,25 @@ class UserMessageTagContent(BaseModel): UserMessageContent = Annotated[ UserMessageTextContent | UserMessageTagContent, Field(discriminator="type") ] +"""Union of allowed user message content payloads.""" class InferenceOptions(BaseModel): + """Model and tool configuration for message processing.""" + tool_choice: ToolChoice | None = None model: str | None = None class ToolChoice(BaseModel): + """Explicit tool selection for the assistant to invoke.""" + id: str class AttachmentBase(BaseModel): + """Base metadata shared by all attachments.""" + id: str name: str mime_type: str @@ -526,10 +672,14 @@ class AttachmentBase(BaseModel): class FileAttachment(AttachmentBase): + """Attachment representing a generic file.""" + type: Literal["file"] = "file" class ImageAttachment(AttachmentBase): + """Attachment representing an image resource.""" + type: Literal["image"] = "image" preview_url: AnyUrl @@ -538,12 +688,15 @@ class ImageAttachment(AttachmentBase): FileAttachment | ImageAttachment, Field(discriminator="type"), ] +"""Union of supported attachment types.""" ### WORKFLOW TYPES class Workflow(BaseModel): + """Workflow attached to a thread with optional summary.""" + type: Literal["custom", "reasoning"] tasks: list[Task] summary: WorkflowSummary | None = None @@ -551,26 +704,35 @@ class Workflow(BaseModel): class CustomSummary(BaseModel): + """Custom summary for a workflow.""" + title: str icon: str | None = None class DurationSummary(BaseModel): + """Summary providing total workflow duration.""" + duration: int """The duration of the workflow in seconds""" WorkflowSummary = CustomSummary | DurationSummary +"""Summary variants available for workflows.""" ### TASK TYPES class BaseTask(BaseModel): + """Base fields common to all workflow tasks.""" + status_indicator: Literal["none", "loading", "complete"] = "none" """Only used when rendering the task as part of a workflow. Indicates the status of the task.""" class CustomTask(BaseTask): + """Workflow task displaying custom content.""" + type: Literal["custom"] = "custom" title: str | None = None icon: str | None = None @@ -578,6 +740,8 @@ class CustomTask(BaseTask): class SearchTask(BaseTask): + """Workflow task representing a web search.""" + type: Literal["web_search"] = "web_search" title: str | None = None title_query: str | None = None @@ -586,18 +750,24 @@ class SearchTask(BaseTask): class ThoughtTask(BaseTask): + """Workflow task capturing assistant reasoning.""" + type: Literal["thought"] = "thought" title: str | None = None content: str class FileTask(BaseTask): + """Workflow task referencing file sources.""" + type: Literal["file"] = "file" title: str | None = None sources: list[FileSource] = Field(default_factory=list) class ImageTask(BaseTask): + """Workflow task rendering image content.""" + type: Literal["image"] = "image" title: str | None = None @@ -606,12 +776,15 @@ class ImageTask(BaseTask): CustomTask | SearchTask | ThoughtTask | FileTask | ImageTask, Field(discriminator="type"), ] +"""Union of workflow task variants.""" ### SOURCE TYPES class SourceBase(BaseModel): + """Base class for sources displayed to users.""" + title: str description: str | None = None timestamp: str | None = None @@ -619,30 +792,42 @@ class SourceBase(BaseModel): class FileSource(SourceBase): + """Source metadata for file-based references.""" + type: Literal["file"] = "file" filename: str class URLSource(SourceBase): + """Source metadata for external URLs.""" + type: Literal["url"] = "url" url: str attribution: str | None = None class EntitySource(SourceBase): + """Source metadata for entity references.""" + type: Literal["entity"] = "entity" id: str icon: str | None = None preview: Literal["lazy"] | None = None -Source = URLSource | FileSource | EntitySource +Source = Annotated[ + URLSource | FileSource | EntitySource, + Field(discriminator="type"), +] +"""Union of supported source types.""" ### MISC TYPES FeedbackKind = Literal["positive", "negative"] +"""Literal type for feedback sentiment.""" + IconName = Literal[ "analytics", @@ -679,3 +864,4 @@ class EntitySource(SourceBase): "write-alt", "write-alt2", ] +"""Literal names of supported progress icons.""" diff --git a/uv.lock b/uv.lock index baba0f6..de2664f 100644 --- a/uv.lock +++ b/uv.lock @@ -149,65 +149,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] -[[package]] -name = "chatkit" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "openai" }, - { name = "openai-agents" }, - { name = "pydantic" }, - { name = "uvicorn" }, -] - -[package.dev-dependencies] -dev = [ - { name = "debugpy" }, - { name = "fastapi" }, - { name = "flask" }, - { name = "mkdocs" }, - { name = "mkdocs-gen-files" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, - { name = "mypy" }, - { name = "psycopg2-binary" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "python-multipart" }, - { name = "ruff" }, -] -lint = [ - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "openai" }, - { name = "openai-agents", specifier = ">=0.3.2" }, - { name = "pydantic" }, - { name = "uvicorn" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "debugpy" }, - { name = "fastapi" }, - { name = "flask" }, - { name = "mkdocs" }, - { name = "mkdocs-gen-files" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extras = ["python"] }, - { name = "mypy" }, - { name = "psycopg2-binary" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "python-multipart" }, - { name = "ruff", specifier = "==0.9.2" }, -] -lint = [{ name = "ruff" }] - [[package]] name = "click" version = "8.3.0" @@ -876,6 +817,65 @@ 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 = "0.0.2" +source = { virtual = "." } +dependencies = [ + { name = "openai" }, + { name = "openai-agents" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "debugpy" }, + { name = "fastapi" }, + { name = "flask" }, + { name = "mkdocs" }, + { name = "mkdocs-gen-files" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "mypy" }, + { name = "psycopg2-binary" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "python-multipart" }, + { name = "ruff" }, +] +lint = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "openai" }, + { name = "openai-agents", specifier = ">=0.3.2" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "debugpy" }, + { name = "fastapi" }, + { name = "flask" }, + { name = "mkdocs" }, + { name = "mkdocs-gen-files" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, + { name = "mypy" }, + { name = "psycopg2-binary" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "python-multipart" }, + { name = "ruff", specifier = "==0.9.2" }, +] +lint = [{ name = "ruff" }] + [[package]] name = "packaging" version = "25.0"