openai/chatkit-python

Public

mirrored fromhttps://github.com/openai/chatkit-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8b263facaeaba1c1035a4d92befabf8f2c75436c

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

chatkit/types.py

1088lines · modecode

1from __future__ import annotations
2
3from datetime import datetime
4from typing import Any, Generic, Literal
5
6from pydantic import (
7 AnyUrl,
8 BaseModel,
9 Field,
10 SerializationInfo,
11 model_serializer,
12)
13from typing_extensions import Annotated, TypeIs, TypeVar
14
15from chatkit.errors import ErrorCode
16
17from .actions import Action
18from .icons import IconName
19from .widgets import WidgetComponent, WidgetRoot
20
21T = TypeVar("T")
22
23
24class Page(BaseModel, Generic[T]):
25 """Paginated collection of records returned from the API."""
26
27 data: list[T] = []
28 has_more: bool = False
29 after: str | None = None
30
31
32### REQUEST TYPES
33
34
35class BaseReq(BaseModel):
36 """Base class for all request payloads."""
37
38 metadata: dict[str, Any] = Field(default_factory=dict)
39 """Arbitrary integration-specific metadata."""
40
41
42class ThreadsGetByIdReq(BaseReq):
43 """Request to fetch a single thread by its identifier."""
44
45 type: Literal["threads.get_by_id"] = "threads.get_by_id"
46 params: ThreadGetByIdParams
47
48
49class ThreadGetByIdParams(BaseModel):
50 """Parameters for retrieving a thread by id."""
51
52 thread_id: str
53
54
55class ThreadsCreateReq(BaseReq):
56 """Request to create a new thread from a user message."""
57
58 type: Literal["threads.create"] = "threads.create"
59 params: ThreadCreateParams
60
61
62class ThreadCreateParams(BaseModel):
63 """User input required to create a thread."""
64
65 input: UserMessageInput
66
67
68class ThreadListParams(BaseModel):
69 """Pagination parameters for listing threads."""
70
71 limit: int | None = None
72 order: Literal["asc", "desc"] = "desc"
73 after: str | None = None
74
75
76class ThreadsListReq(BaseReq):
77 """Request to list threads."""
78
79 type: Literal["threads.list"] = "threads.list"
80 params: ThreadListParams
81
82
83class ThreadsAddUserMessageReq(BaseReq):
84 """Request to append a user message to a thread."""
85
86 type: Literal["threads.add_user_message"] = "threads.add_user_message"
87 params: ThreadAddUserMessageParams
88
89
90class ThreadAddUserMessageParams(BaseModel):
91 """Parameters for adding a user message to a thread."""
92
93 input: UserMessageInput
94 thread_id: str
95
96
97class ThreadsAddClientToolOutputReq(BaseReq):
98 """Request to add a client tool's output to a thread."""
99
100 type: Literal["threads.add_client_tool_output"] = "threads.add_client_tool_output"
101 params: ThreadAddClientToolOutputParams
102
103
104class ThreadAddClientToolOutputParams(BaseModel):
105 """Parameters for recording tool output in a thread."""
106
107 thread_id: str
108 result: Any
109
110
111class StructuredInputAnswerSubmission(BaseModel):
112 """Client-submitted answer for a structured input."""
113
114 values: list[str] = Field(default_factory=list)
115 """Text answer values submitted for the structured input."""
116 skipped: bool = False
117 """Whether this structured input answer was skipped."""
118
119
120class StructuredInputSubmission(BaseModel):
121 """Client-submitted answers for a pending structured input item."""
122
123 status: Literal["answered", "skipped"] = "answered"
124 """Overall status for the structured input submission."""
125 answers: dict[str, StructuredInputAnswerSubmission] = Field(default_factory=dict)
126 """Answers keyed by structured input id."""
127
128
129class ThreadsAddStructuredInputReq(BaseReq):
130 """Request to submit answers for a pending structured input item."""
131
132 type: Literal["threads.add_structured_input"] = "threads.add_structured_input"
133 params: ThreadAddStructuredInputParams
134
135
136class ThreadAddStructuredInputParams(BaseModel):
137 """Parameters for adding structured input to a thread."""
138
139 thread_id: str
140 item_id: str
141 input: StructuredInputSubmission
142
143
144class ThreadsCustomActionReq(BaseReq):
145 """Request to execute a custom action within a thread."""
146
147 type: Literal["threads.custom_action"] = "threads.custom_action"
148 params: ThreadCustomActionParams
149
150
151class ThreadsSyncCustomActionReq(BaseReq):
152 """Request to execute a custom action and return a single item update."""
153
154 type: Literal["threads.sync_custom_action"] = "threads.sync_custom_action"
155 params: ThreadCustomActionParams
156
157
158class ThreadCustomActionParams(BaseModel):
159 """Parameters describing the custom action to execute."""
160
161 thread_id: str
162 item_id: str | None = None
163 action: Action[str, Any]
164
165
166class ThreadsRetryAfterItemReq(BaseReq):
167 """Request to retry processing after a specific thread item."""
168
169 type: Literal["threads.retry_after_item"] = "threads.retry_after_item"
170 params: ThreadRetryAfterItemParams
171
172
173class ThreadRetryAfterItemParams(BaseModel):
174 """Parameters specifying which item to retry."""
175
176 thread_id: str
177 item_id: str
178
179
180class ItemsFeedbackReq(BaseReq):
181 """Request to submit feedback on specific items."""
182
183 type: Literal["items.feedback"] = "items.feedback"
184 params: ItemFeedbackParams
185
186
187class ItemFeedbackParams(BaseModel):
188 """Parameters describing feedback targets and sentiment."""
189
190 thread_id: str
191 item_ids: list[str]
192 kind: FeedbackKind
193
194
195class AttachmentsDeleteReq(BaseReq):
196 """Request to remove an attachment."""
197
198 type: Literal["attachments.delete"] = "attachments.delete"
199 params: AttachmentDeleteParams
200
201
202class AttachmentDeleteParams(BaseModel):
203 """Parameters identifying an attachment to delete."""
204
205 attachment_id: str
206
207
208class AttachmentsCreateReq(BaseReq):
209 """Request to register a new attachment."""
210
211 type: Literal["attachments.create"] = "attachments.create"
212 params: AttachmentCreateParams
213
214
215class AttachmentCreateParams(BaseModel):
216 """Metadata needed to initialize an attachment."""
217
218 name: str
219 size: int
220 mime_type: str
221
222
223class InputTranscribeReq(BaseReq):
224 """Request to transcribe an audio payload into text."""
225
226 type: Literal["input.transcribe"] = "input.transcribe"
227 params: InputTranscribeParams
228
229
230class InputTranscribeParams(BaseModel):
231 """Parameters for speech transcription."""
232
233 audio_base64: str
234 """Base64-encoded audio bytes."""
235
236 mime_type: str
237 """Raw MIME type for the audio payload, e.g. "audio/webm;codecs=opus"."""
238
239
240class AudioInput(BaseModel):
241 """Audio input data for transcription."""
242
243 data: bytes
244 """Audio data bytes."""
245
246 mime_type: str
247 """Raw MIME type for the audio payload, e.g. "audio/webm;codecs=opus"."""
248
249 @property
250 def media_type(self) -> str:
251 """Media type for the audio payload, e.g. "audio/webm"."""
252 return self.mime_type.split(";", 1)[0]
253
254
255class TranscriptionResult(BaseModel):
256 """Input speech transcription result."""
257
258 text: str
259
260
261class ItemsListReq(BaseReq):
262 """Request to list items inside a thread."""
263
264 type: Literal["items.list"] = "items.list"
265 params: ItemsListParams
266
267
268class ItemsListParams(BaseModel):
269 """Pagination parameters for listing thread items."""
270
271 thread_id: str
272 limit: int | None = None
273 order: Literal["asc", "desc"] = "desc"
274 after: str | None = None
275
276
277class ThreadsUpdateReq(BaseReq):
278 """Request to update thread metadata."""
279
280 type: Literal["threads.update"] = "threads.update"
281 params: ThreadUpdateParams
282
283
284class ThreadUpdateParams(BaseModel):
285 """Parameters for updating a thread's properties."""
286
287 thread_id: str
288 title: str
289
290
291class ThreadsDeleteReq(BaseReq):
292 """Request to delete a thread."""
293
294 type: Literal["threads.delete"] = "threads.delete"
295 params: ThreadDeleteParams
296
297
298class ThreadDeleteParams(BaseModel):
299 """Parameters identifying a thread to delete."""
300
301 thread_id: str
302
303
304StreamingReq = (
305 ThreadsCreateReq
306 | ThreadsAddUserMessageReq
307 | ThreadsAddClientToolOutputReq
308 | ThreadsAddStructuredInputReq
309 | ThreadsRetryAfterItemReq
310 | ThreadsCustomActionReq
311)
312"""Union of request types that produce streaming responses."""
313
314
315NonStreamingReq = (
316 ThreadsGetByIdReq
317 | ThreadsListReq
318 | ItemsListReq
319 | ItemsFeedbackReq
320 | AttachmentsCreateReq
321 | AttachmentsDeleteReq
322 | ThreadsUpdateReq
323 | ThreadsDeleteReq
324 | InputTranscribeReq
325 | ThreadsSyncCustomActionReq
326)
327"""Union of request types that yield immediate responses."""
328
329
330ChatKitReq = Annotated[
331 StreamingReq | NonStreamingReq,
332 Field(discriminator="type"),
333]
334
335
336def is_streaming_req(request: ChatKitReq) -> TypeIs[StreamingReq]:
337 """Return True if the given request should be processed as streaming."""
338 return isinstance(
339 request,
340 (
341 ThreadsCreateReq,
342 ThreadsAddUserMessageReq,
343 ThreadsRetryAfterItemReq,
344 ThreadsAddClientToolOutputReq,
345 ThreadsAddStructuredInputReq,
346 ThreadsCustomActionReq,
347 ),
348 )
349
350
351### THREAD STREAM EVENT TYPES
352
353
354class ThreadCreatedEvent(BaseModel):
355 """Event emitted when a thread is created."""
356
357 type: Literal["thread.created"] = "thread.created"
358 thread: Thread
359
360
361class ThreadUpdatedEvent(BaseModel):
362 """Event emitted when a thread is updated."""
363
364 type: Literal["thread.updated"] = "thread.updated"
365 thread: Thread
366
367
368class ThreadItemAddedEvent(BaseModel):
369 """Event emitted when a new item is added to a thread."""
370
371 type: Literal["thread.item.added"] = "thread.item.added"
372 item: ThreadItem
373
374
375class ThreadItemUpdatedEvent(BaseModel):
376 """Event describing an update to an existing thread item."""
377
378 type: Literal["thread.item.updated"] = "thread.item.updated"
379 item_id: str
380 update: ThreadItemUpdate
381
382
383# Type alias for backwards compatibility
384ThreadItemUpdated = ThreadItemUpdatedEvent
385
386
387class ThreadItemDoneEvent(BaseModel):
388 """Event emitted when a thread item is marked complete."""
389
390 type: Literal["thread.item.done"] = "thread.item.done"
391 item: ThreadItem
392
393
394class ThreadItemRemovedEvent(BaseModel):
395 """Event emitted when a thread item is removed."""
396
397 type: Literal["thread.item.removed"] = "thread.item.removed"
398 item_id: str
399
400
401class ThreadItemReplacedEvent(BaseModel):
402 """Event emitted when a thread item is replaced."""
403
404 type: Literal["thread.item.replaced"] = "thread.item.replaced"
405 item: ThreadItem
406
407
408class StreamOptions(BaseModel):
409 """Settings that control runtime stream behavior."""
410
411 allow_cancel: bool
412 """Allow the client to request cancellation mid-stream."""
413
414
415class StreamOptionsEvent(BaseModel):
416 """Event emitted to set stream options at runtime."""
417
418 type: Literal["stream_options"] = "stream_options"
419 stream_options: StreamOptions
420
421
422class ProgressUpdateEvent(BaseModel):
423 """Event providing incremental progress from the assistant."""
424
425 type: Literal["progress_update"] = "progress_update"
426 icon: IconName | None = None
427 text: str
428
429
430class ClientEffectEvent(BaseModel):
431 """Event emitted to trigger a client side-effect."""
432
433 type: Literal["client_effect"] = "client_effect"
434 name: str
435 data: dict[str, Any] = Field(default_factory=dict)
436
437
438class ErrorEvent(BaseModel):
439 """Event indicating an error occurred while processing a thread."""
440
441 type: Literal["error"] = "error"
442 code: ErrorCode | Literal["custom"] = Field(default="custom")
443 message: str | None = None
444 allow_retry: bool = Field(default=False)
445
446
447class NoticeEvent(BaseModel):
448 """Event conveying a user-facing notice."""
449
450 type: Literal["notice"] = "notice"
451 level: Literal["info", "warning", "danger"]
452 message: str
453 """
454 Supports markdown e.g. "You've reached your limit of 100 messages. [Upgrade](https://...) to a paid plan."
455 """
456 title: str | None = None
457
458
459ThreadStreamEvent = Annotated[
460 ThreadCreatedEvent
461 | ThreadUpdatedEvent
462 | ThreadItemDoneEvent
463 | ThreadItemAddedEvent
464 | ThreadItemUpdated
465 | ThreadItemRemovedEvent
466 | ThreadItemReplacedEvent
467 | StreamOptionsEvent
468 | ProgressUpdateEvent
469 | ClientEffectEvent
470 | ErrorEvent
471 | NoticeEvent,
472 Field(discriminator="type"),
473]
474"""Union of all streaming events emitted to clients."""
475
476### THREAD ITEM UPDATE TYPES
477
478
479class AssistantMessageContentPartAdded(BaseModel):
480 """Event emitted when new assistant content is appended."""
481
482 type: Literal["assistant_message.content_part.added"] = (
483 "assistant_message.content_part.added"
484 )
485 content_index: int
486 content: AssistantMessageContent
487
488
489class AssistantMessageContentPartTextDelta(BaseModel):
490 """Event carrying incremental assistant text output."""
491
492 type: Literal["assistant_message.content_part.text_delta"] = (
493 "assistant_message.content_part.text_delta"
494 )
495 content_index: int
496 delta: str
497
498
499class AssistantMessageContentPartAnnotationAdded(BaseModel):
500 """Event announcing a new annotation on assistant content."""
501
502 type: Literal["assistant_message.content_part.annotation_added"] = (
503 "assistant_message.content_part.annotation_added"
504 )
505 content_index: int
506 annotation_index: int
507 annotation: Annotation
508
509
510class AssistantMessageContentPartDone(BaseModel):
511 """Event indicating an assistant content part is finalized."""
512
513 type: Literal["assistant_message.content_part.done"] = (
514 "assistant_message.content_part.done"
515 )
516 content_index: int
517 content: AssistantMessageContent
518
519
520class WidgetStreamingTextValueDelta(BaseModel):
521 """Event streaming widget text deltas."""
522
523 type: Literal["widget.streaming_text.value_delta"] = (
524 "widget.streaming_text.value_delta"
525 )
526 component_id: str
527 delta: str
528 done: bool
529
530
531class WidgetRootUpdated(BaseModel):
532 """Event published when the widget root changes."""
533
534 type: Literal["widget.root.updated"] = "widget.root.updated"
535 widget: WidgetRoot
536
537
538class WidgetComponentUpdated(BaseModel):
539 """Event emitted when a widget component updates."""
540
541 type: Literal["widget.component.updated"] = "widget.component.updated"
542 component_id: str
543 component: WidgetComponent
544
545
546class WorkflowTaskAdded(BaseModel):
547 """Event emitted when a workflow task is added."""
548
549 type: Literal["workflow.task.added"] = "workflow.task.added"
550 task_index: int
551 task: Task
552
553
554class WorkflowTaskUpdated(BaseModel):
555 """Event emitted when a workflow task is updated."""
556
557 type: Literal["workflow.task.updated"] = "workflow.task.updated"
558 task_index: int
559 task: Task
560
561
562class GeneratedImageUpdated(BaseModel):
563 """Event emitted when a generated image is updated."""
564
565 type: Literal["generated_image.updated"] = "generated_image.updated"
566 image: GeneratedImage
567 progress: float | None = None
568
569
570ThreadItemUpdate = (
571 AssistantMessageContentPartAdded
572 | AssistantMessageContentPartTextDelta
573 | AssistantMessageContentPartAnnotationAdded
574 | AssistantMessageContentPartDone
575 | WidgetStreamingTextValueDelta
576 | WidgetComponentUpdated
577 | WidgetRootUpdated
578 | WorkflowTaskAdded
579 | WorkflowTaskUpdated
580 | GeneratedImageUpdated
581)
582"""Union of possible updates applied to thread items."""
583
584
585class SyncCustomActionResponse(BaseModel):
586 """Single thread item update returned by a sync custom action."""
587 updated_item: ThreadItem | None = None
588
589
590### THREAD TYPES
591
592
593class ThreadMetadata(BaseModel):
594 """Metadata describing a thread without its items."""
595
596 title: str | None = None
597 id: str
598 created_at: datetime
599 status: ThreadStatus = Field(default_factory=lambda: ActiveStatus())
600 allowed_image_domains: list[str] | None = None
601 metadata: dict[str, Any] = Field(default_factory=dict)
602
603
604class ActiveStatus(BaseModel):
605 """Status indicating the thread is active."""
606
607 type: Literal["active"] = Field(default="active", frozen=True)
608
609
610class LockedStatus(BaseModel):
611 """Status indicating the thread is locked."""
612
613 type: Literal["locked"] = Field(default="locked", frozen=True)
614 reason: str | None = None
615
616
617class ClosedStatus(BaseModel):
618 """Status indicating the thread is closed."""
619
620 type: Literal["closed"] = Field(default="closed", frozen=True)
621 reason: str | None = None
622
623
624ThreadStatus = Annotated[
625 ActiveStatus | LockedStatus | ClosedStatus,
626 Field(discriminator="type"),
627]
628"""Union of lifecycle states for a thread."""
629
630
631class Thread(ThreadMetadata):
632 """Thread with its paginated items."""
633
634 items: Page[ThreadItem]
635
636
637### THREAD ITEM TYPES
638
639
640class ThreadItemBase(BaseModel):
641 """Base fields shared by all thread items."""
642
643 id: str
644 thread_id: str
645 created_at: datetime
646
647
648class UserMessageItem(ThreadItemBase):
649 """Thread item representing a user message."""
650
651 type: Literal["user_message"] = "user_message"
652 content: list[UserMessageContent]
653 attachments: list[Attachment] = Field(default_factory=list)
654 quoted_text: str | None = None
655 inference_options: InferenceOptions
656
657
658class AssistantMessageItem(ThreadItemBase):
659 """Thread item representing an assistant message."""
660
661 type: Literal["assistant_message"] = "assistant_message"
662 content: list[AssistantMessageContent]
663
664
665class ClientToolCallItem(ThreadItemBase):
666 """Thread item capturing a client tool call."""
667
668 type: Literal["client_tool_call"] = "client_tool_call"
669 status: Literal["pending", "completed"] = "pending"
670 call_id: str
671 name: str
672 arguments: dict[str, Any]
673 output: Any | None = None
674
675
676class WidgetItem(ThreadItemBase):
677 """Thread item containing widget content."""
678
679 type: Literal["widget"] = "widget"
680 widget: WidgetRoot
681 copy_text: str | None = None
682
683
684class GeneratedImage(BaseModel):
685 """Generated image."""
686
687 id: str
688 url: str
689
690
691class GeneratedImageItem(ThreadItemBase):
692 """Thread item containing a generated image."""
693
694 type: Literal["generated_image"] = "generated_image"
695 image: GeneratedImage | None = None
696
697
698class StructuredInputAnswer(BaseModel):
699 """Answer recorded for a structured input."""
700
701 values: list[str] = Field(default_factory=list)
702 """Text answer values recorded for the structured input."""
703 skipped: bool = False
704 """Whether this structured input answer was skipped."""
705
706
707class StructuredInputBase(BaseModel):
708 """Base fields shared by structured inputs."""
709
710 id: str
711 """Stable id for this structured input."""
712 question: str
713 """Question shown to the user."""
714 answer: StructuredInputAnswer | None = None
715 """Answer recorded for this structured input, if available."""
716
717
718class StructuredInputMultipleChoice(StructuredInputBase):
719 """Structured input answered by choosing one or more options."""
720
721 type: Literal["multiple_choice"] = "multiple_choice"
722 options: list[str]
723 """Suggested choices to display before the freeform custom answer affordance."""
724 multiple: bool = False
725 """Whether the user may submit more than one text value."""
726
727
728class StructuredInputFreeform(StructuredInputBase):
729 """Structured input answered with freeform text."""
730
731 type: Literal["freeform"] = "freeform"
732 description: str | None = None
733 """Supporting text shown with this input."""
734
735
736StructuredInput = Annotated[
737 StructuredInputMultipleChoice | StructuredInputFreeform,
738 Field(discriminator="type"),
739]
740"""Structured input variants supported by a structured input item."""
741
742
743class StructuredInputItem(ThreadItemBase):
744 """Thread item requesting structured input from the user."""
745
746 type: Literal["structured_input"] = "structured_input"
747 status: Literal["pending", "answered", "skipped"] = "pending"
748 inputs: list[StructuredInput]
749
750
751class TaskItem(ThreadItemBase):
752 """Thread item containing a task."""
753
754 type: Literal["task"] = "task"
755 task: Task
756
757
758class WorkflowItem(ThreadItemBase):
759 """Thread item representing a workflow."""
760
761 type: Literal["workflow"] = "workflow"
762 workflow: Workflow
763
764
765class EndOfTurnItem(ThreadItemBase):
766 """Marker item indicating the assistant ends its turn."""
767
768 type: Literal["end_of_turn"] = "end_of_turn"
769
770
771class HiddenContextItem(ThreadItemBase):
772 """
773 HiddenContext is never sent to the client. It's not officially part of ChatKit.js.
774 It is only used internally to store additional context in a specific place in the thread.
775 """
776
777 type: Literal["hidden_context_item"] = "hidden_context_item"
778 content: Any
779
780
781class SDKHiddenContextItem(ThreadItemBase):
782 """
783 Hidden context that is used by the ChatKit Python SDK for storing additional context
784 for internal operations.
785 """
786
787 type: Literal["sdk_hidden_context"] = "sdk_hidden_context"
788 content: str
789
790
791ThreadItem = Annotated[
792 UserMessageItem
793 | AssistantMessageItem
794 | ClientToolCallItem
795 | WidgetItem
796 | GeneratedImageItem
797 | StructuredInputItem
798 | WorkflowItem
799 | TaskItem
800 | HiddenContextItem
801 | SDKHiddenContextItem
802 | EndOfTurnItem,
803 Field(discriminator="type"),
804]
805"""Union of all thread item variants."""
806
807
808### ASSISTANT MESSAGE TYPES
809
810
811class AssistantMessageContent(BaseModel):
812 """Assistant message content consisting of text and annotations."""
813
814 annotations: list[Annotation] = Field(default_factory=list)
815 text: str
816 type: Literal["output_text"] = "output_text"
817
818
819class Annotation(BaseModel):
820 """Reference to supporting context attached to assistant output."""
821
822 type: Literal["annotation"] = "annotation"
823 source: URLSource | FileSource | EntitySource
824 index: int | None = None
825
826
827### USER MESSAGE TYPES
828
829
830class UserMessageInput(BaseModel):
831 """Payload describing a user message submission."""
832
833 content: list[UserMessageContent]
834 attachments: list[str]
835 quoted_text: str | None = None
836 inference_options: InferenceOptions
837
838
839class UserMessageTextContent(BaseModel):
840 """User message content containing plaintext."""
841
842 type: Literal["input_text"] = "input_text"
843 text: str
844
845
846class UserMessageTagContent(BaseModel):
847 """User message content representing an interactive tag."""
848
849 type: Literal["input_tag"] = "input_tag"
850 id: str
851 text: str
852 data: dict[str, Any]
853 group: str | None = None
854 interactive: bool = False
855
856
857UserMessageContent = Annotated[
858 UserMessageTextContent | UserMessageTagContent, Field(discriminator="type")
859]
860"""Union of allowed user message content payloads."""
861
862
863class InferenceOptions(BaseModel):
864 """Model and tool configuration for message processing."""
865
866 tool_choice: ToolChoice | None = None
867 model: str | None = None
868
869
870class ToolChoice(BaseModel):
871 """Explicit tool selection for the assistant to invoke."""
872
873 id: str
874
875
876class AttachmentUploadDescriptor(BaseModel):
877 """Two-phase upload instructions."""
878
879 url: AnyUrl
880 method: Literal["POST", "PUT"]
881 """The HTTP method to use when uploading the file for two-phase upload."""
882 headers: dict[str, str] = Field(default_factory=dict)
883 """Optional headers to include in the upload request."""
884
885
886class AttachmentBase(BaseModel):
887 """Base metadata shared by all attachments."""
888
889 id: str
890 name: str
891 mime_type: str
892 upload_descriptor: AttachmentUploadDescriptor | None = None
893 """
894 Two-phase upload instructions.
895 Should be set to None after upload is complete or when using direct upload
896 where uploading happens when creating the attachment object.
897 """
898 thread_id: str | None = None
899 """
900 The thread the attachment belongs to, if any.
901 Added when the user message that contains the attachment is saved to store.
902 """
903 metadata: dict[str, Any] | None = None
904 """
905 Integration-only metadata stored with the attachment. Ignored by ChatKit and not
906 returned in ChatKitServer responses. If you serialize attachments from a custom
907 direct-upload endpoint and want to omit this field, pass context={"exclude_metadata": True}.
908 """
909
910 @model_serializer(mode="wrap")
911 def _serialize(self, serializer, info: SerializationInfo):
912 data = serializer(self)
913 if isinstance(data, dict) and (info.context or {}).get("exclude_metadata"):
914 data.pop("metadata", None)
915 return data
916
917
918class FileAttachment(AttachmentBase):
919 """Attachment representing a generic file."""
920
921 type: Literal["file"] = "file"
922
923
924class ImageAttachment(AttachmentBase):
925 """Attachment representing an image resource."""
926
927 type: Literal["image"] = "image"
928 preview_url: AnyUrl
929
930
931Attachment = Annotated[
932 FileAttachment | ImageAttachment,
933 Field(discriminator="type"),
934]
935"""Union of supported attachment types."""
936
937
938### WORKFLOW TYPES
939
940
941class Workflow(BaseModel):
942 """Workflow attached to a thread with optional summary."""
943
944 type: Literal["custom", "reasoning"]
945 tasks: list[Task]
946 summary: WorkflowSummary | None = None
947 expanded: bool = False
948
949
950class CustomSummary(BaseModel):
951 """Custom summary for a workflow."""
952
953 title: str
954 icon: IconName | None = None
955
956
957class DurationSummary(BaseModel):
958 """Summary providing total workflow duration."""
959
960 duration: int
961 """The duration of the workflow in seconds"""
962
963
964WorkflowSummary = CustomSummary | DurationSummary
965"""Summary variants available for workflows."""
966
967### TASK TYPES
968
969
970class BaseTask(BaseModel):
971 """Base fields common to all workflow tasks."""
972
973 status_indicator: Literal["none", "loading", "complete"] = "none"
974 """Only used when rendering the task as part of a workflow. Indicates the status of the task."""
975
976
977class CustomTask(BaseTask):
978 """Workflow task displaying custom content."""
979
980 type: Literal["custom"] = "custom"
981 title: str | None = None
982 icon: IconName | None = None
983 content: str | None = None
984
985
986class SearchTask(BaseTask):
987 """Workflow task representing a web search."""
988
989 type: Literal["web_search"] = "web_search"
990 title: str | None = None
991 title_query: str | None = None
992 queries: list[str] = Field(default_factory=list)
993 sources: list[URLSource] = Field(default_factory=list)
994
995
996class ThoughtTask(BaseTask):
997 """Workflow task capturing assistant reasoning."""
998
999 type: Literal["thought"] = "thought"
1000 title: str | None = None
1001 content: str
1002
1003
1004class FileTask(BaseTask):
1005 """Workflow task referencing file sources."""
1006
1007 type: Literal["file"] = "file"
1008 title: str | None = None
1009 sources: list[FileSource] = Field(default_factory=list)
1010
1011
1012class ImageTask(BaseTask):
1013 """Workflow task rendering image content."""
1014
1015 type: Literal["image"] = "image"
1016 title: str | None = None
1017
1018
1019Task = Annotated[
1020 CustomTask | SearchTask | ThoughtTask | FileTask | ImageTask,
1021 Field(discriminator="type"),
1022]
1023"""Union of workflow task variants."""
1024
1025
1026### SOURCE TYPES
1027
1028
1029class SourceBase(BaseModel):
1030 """Base class for sources displayed to users."""
1031
1032 title: str
1033 description: str | None = None
1034 timestamp: str | None = None
1035 group: str | None = None
1036
1037
1038class FileSource(SourceBase):
1039 """Source metadata for file-based references."""
1040
1041 type: Literal["file"] = "file"
1042 filename: str
1043
1044
1045class URLSource(SourceBase):
1046 """Source metadata for external URLs."""
1047
1048 type: Literal["url"] = "url"
1049 url: str
1050 attribution: str | None = None
1051
1052
1053class EntitySource(SourceBase):
1054 """Source metadata for entity references."""
1055
1056 type: Literal["entity"] = "entity"
1057 id: str
1058 icon: IconName | None = None
1059 label: str | None = None
1060 """Optional label shown with the icon in the default entity hover header
1061 when no preview callback is provided.
1062 """
1063 inline_label: str | None = None
1064 """Optional label for the inline annotation view. When not provided, the icon is used instead."""
1065 interactive: bool = False
1066 """Per-entity toggle to wire client callbacks and render this entity as interactive."""
1067 data: dict[str, Any] = Field(default_factory=dict)
1068 """Additional data for the entity source that is passed to client entity callbacks."""
1069
1070 preview: Literal["lazy"] | None = Field(
1071 default=None,
1072 deprecated=True,
1073 description="This field is ignored. Please use the entities.onRequestPreview ChatKit.js option instead.",
1074 )
1075
1076
1077Source = Annotated[
1078 URLSource | FileSource | EntitySource,
1079 Field(discriminator="type"),
1080]
1081"""Union of supported source types."""
1082
1083
1084### MISC TYPES
1085
1086
1087FeedbackKind = Literal["positive", "negative"]
1088"""Literal type for feedback sentiment."""
1089