Skip to content

Commit 1913e50

Browse files
authored
Merge pull request #106 from openai/add-attachment-metadata
Add a metadata field on `AttachmentBase` for integration-internal use cases
2 parents a4eead1 + 5659def commit 1913e50

6 files changed

Lines changed: 75 additions & 5 deletions

File tree

chatkit/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,11 @@ async def _paginate_thread_items_reverse(
878878
after = items.after
879879

880880
def _serialize(self, obj: BaseModel) -> bytes:
881-
return obj.model_dump_json(by_alias=True, exclude_none=True).encode("utf-8")
881+
return obj.model_dump_json(
882+
by_alias=True,
883+
exclude_none=True,
884+
context={"exclude_metadata": True},
885+
).encode("utf-8")
882886

883887
def _to_thread_response(self, thread: ThreadMetadata | Thread) -> Thread:
884888
def is_hidden(item: ThreadItem) -> bool:

chatkit/types.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44
from typing import Any, Generic, Literal
55

6-
from pydantic import AnyUrl, BaseModel, Field
6+
from pydantic import AnyUrl, BaseModel, Field, SerializationInfo, model_serializer
77
from typing_extensions import Annotated, TypeIs, TypeVar
88

99
from chatkit.errors import ErrorCode
@@ -748,6 +748,19 @@ class AttachmentBase(BaseModel):
748748
Should be set to None after upload is complete or when using direct upload
749749
where uploading happens when creating the attachment object.
750750
"""
751+
metadata: dict[str, Any] | None = None
752+
"""
753+
Integration-only metadata stored with the attachment. Ignored by ChatKit and not
754+
returned in ChatKitServer responses. If you serialize attachments from a custom
755+
direct-upload endpoint and want to omit this field, pass context={"exclude_metadata": True}.
756+
"""
757+
758+
@model_serializer(mode="wrap")
759+
def _serialize(self, serializer, info: SerializationInfo):
760+
data = serializer(self)
761+
if isinstance(data, dict) and (info.context or {}).get("exclude_metadata"):
762+
data.pop("metadata", None)
763+
return data
751764

752765

753766
class FileAttachment(AttachmentBase):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "openai-chatkit"
3-
version = "1.5.2"
3+
version = "1.5.3"
44
description = "A ChatKit backend SDK."
55
readme = "README.md"
66
requires-python = ">=3.10"

tests/test_chatkit_server.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ async def create_attachment(
106106
) -> Attachment:
107107
# Simple in-memory attachment creation
108108
id = f"atc_{len(self.files) + 1}"
109+
metadata = {"source": "test"}
109110
if input.mime_type.startswith("image/"):
110111
attachment = ImageAttachment(
111112
id=id,
@@ -117,6 +118,7 @@ async def create_attachment(
117118
method="PUT",
118119
headers={"X-My-Header": "my-value"},
119120
),
121+
metadata=metadata,
120122
)
121123
else:
122124
attachment = FileAttachment(
@@ -128,6 +130,7 @@ async def create_attachment(
128130
method="PUT",
129131
headers={"X-My-Header": "my-value"},
130132
),
133+
metadata=metadata,
131134
)
132135
self.files[attachment.id] = attachment
133136
return attachment
@@ -1100,6 +1103,10 @@ async def test_create_file():
11001103
assert attachment.upload_descriptor.headers == {"X-My-Header": "my-value"}
11011104
assert attachment.id in store.files
11021105

1106+
# The `metadata` field is excluded from serialization by default but still stored server-side
1107+
assert attachment.metadata is None
1108+
assert store.files[attachment.id].metadata == {"source": "test"}
1109+
11031110

11041111
async def test_create_image_file():
11051112
store = InMemoryFileStore()
@@ -1135,6 +1142,43 @@ async def test_create_image_file():
11351142
assert attachment.id in store.files
11361143

11371144

1145+
async def test_user_message_attachment_metadata_not_serialized():
1146+
attachment = FileAttachment(
1147+
id="file_with_metadata",
1148+
type="file",
1149+
mime_type="text/plain",
1150+
name="test.txt",
1151+
metadata={"source": "test"},
1152+
)
1153+
1154+
with make_server() as server:
1155+
await server.store.save_attachment(attachment, DEFAULT_CONTEXT)
1156+
events = await server.process_streaming(
1157+
ThreadsCreateReq(
1158+
params=ThreadCreateParams(
1159+
input=UserMessageInput(
1160+
content=[UserMessageTextContent(text="Message with file")],
1161+
attachments=[attachment.id],
1162+
inference_options=InferenceOptions(),
1163+
)
1164+
)
1165+
)
1166+
)
1167+
thread = next(
1168+
event.thread for event in events if event.type == "thread.created"
1169+
)
1170+
1171+
list_result = await server.process_non_streaming(
1172+
ItemsListReq(params=ItemsListParams(thread_id=thread.id))
1173+
)
1174+
items = TypeAdapter(Page[ThreadItem]).validate_json(list_result.json)
1175+
user_items = [item for item in items.data if item.type == "user_message"]
1176+
assert user_items
1177+
attachments = user_items[0].attachments
1178+
assert attachments
1179+
assert all(att.metadata is None for att in attachments)
1180+
1181+
11381182
async def test_create_file_without_filestore():
11391183
with pytest.raises(RuntimeError):
11401184
with make_server() as server:

tests/test_store.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,15 @@ def make_thread_items() -> list[ThreadItem]:
3939
user_msg = UserMessageItem(
4040
id="msg_100000",
4141
content=[UserMessageTextContent(text="Hello!")],
42-
attachments=[],
42+
attachments=[
43+
FileAttachment(
44+
id="file_1",
45+
type="file",
46+
mime_type="text/plain",
47+
name="test.txt",
48+
metadata={"source": "test"},
49+
)
50+
],
4351
inference_options=InferenceOptions(),
4452
thread_id="test_thread",
4553
created_at=now,
@@ -169,6 +177,7 @@ async def test_save_and_load_image(self):
169177
mime_type="image/png",
170178
name="test.png",
171179
preview_url=AnyUrl("https://example.com/test.png"),
180+
metadata={"source": "test"},
172181
)
173182
await self.store.save_attachment(image, DEFAULT_CONTEXT)
174183
loaded = await self.store.load_attachment(image.id, DEFAULT_CONTEXT)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)