feat: add multipart memory uploads

This commit is contained in:
2026-06-22 15:53:29 +08:00
parent 12c767cd68
commit f77454b4cc
6 changed files with 1076 additions and 1255 deletions

View File

@ -1,144 +1,126 @@
# Memory Gateway multimodal API test
# Memory Gateway API curl examples
This file records a real end-to-end test through **Memory Gateway**, not direct upstream memory service calls.
This file keeps only the concrete API curl shapes and short notes. Replace
`<USER_KEY>` with the key returned by `POST /users`.
Gateway URL used by curl:
Base URL used in the live deployment test:
```text
http://127.0.0.1:8010
```
Gateway upstream memory service:
Test files:
```text
http://10.6.80.123:1995
tests/simple-multimodal-image.png
tests/simple-tone.wav
```
Test assets:
```text
/home/tom/memory-gateway/tests/simple-multimodal-image.png
/home/tom/memory-gateway/tests/simple-tone.wav
```
Asset check:
```text
tests/simple-multimodal-image.png: PNG image data, 96 x 64, 8-bit/color RGB, non-interlaced
tests/simple-tone.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 8000 Hz
```
## Start Gateway
Command:
## 1. Health
```bash
cd /home/tom/memory-gateway
MEMORY_GATEWAY_BACKEND_BASE_URL=http://10.6.80.123:1995 \
MEMORY_GATEWAY_DB_PATH=/tmp/memory_gateway_curl.sqlite3 \
MEMORY_GATEWAY_STORAGE_DIR=/tmp/memory_gateway_curl_storage \
MEMORY_GATEWAY_HOST=127.0.0.1 \
MEMORY_GATEWAY_PORT=8010 \
.venv/bin/python main.py
curl -sS http://127.0.0.1:8010/health
```
Observed startup:
```text
INFO: Started server process [771099]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8010 (Press CTRL+C to quit)
```
## 1. Create Gateway user
Request:
```bash
USER_ID="gateway_user_20260611180257"
curl -sS --location 'http://127.0.0.1:8010/users' \
--header 'Content-Type: application/json' \
--data "{
\"user_id\": \"${USER_ID}\"
}"
```
Response:
Expected shape:
```json
{
"user_id": "gateway_user_20260611180257",
"user_key": "uk_REDACTED",
"created_at": "2026-06-11T10:02:57.435437+00:00"
"status": "ok",
"api": {"status": "ok"},
"backend": {
"status": "ok",
"base_url": "http://0.0.0.0:1995",
"data": {"status": "ok"}
}
}
```
HTTP metadata:
## 2. Create user
```text
HTTP_STATUS:200
TOTAL_TIME:0.022431
```bash
curl -sS -X POST http://127.0.0.1:8010/users \
-H 'Content-Type: application/json' \
-d '{"user_id":"gateway_demo_user"}'
```
## 2. Add text + audio(base64) + image(file) through Gateway
Expected shape:
```json
{
"user_id": "gateway_demo_user",
"user_key": "uk_REDACTED",
"created_at": "2026-06-22T06:54:35.823262+00:00"
}
```
Use the returned `user_key` in later requests.
## 3. Add chat memory with multipart files
Use this when files belong to a chat/session message and the client should not
or cannot convert the files to base64.
`upload_id` rules:
- `upload_id` is defined by the caller.
- Gateway does not generate it.
- Gateway does not require a format such as `user_id_filetype_number`.
- It only needs to be non-empty, unique inside the request, and equal to the
multipart file field name.
- Good simple values are `image_1`, `image_2`, `audio_1`, `doc_1`.
In the `messages` JSON, `upload_id: "image_1"` points to this file field:
```bash
-F 'image_1=@tests/simple-multimodal-image.png;type=image/png'
```
Request:
```bash
cd /home/tom/memory-gateway
USER_KEY="uk_REDACTED"
CONVERSATION_ID="gateway-multimodal-20260611180257"
SESSION_ID="chat:${CONVERSATION_ID}"
TIMESTAMP_MS="1781172177000"
AUDIO_BASE64="$(base64 -w0 tests/simple-tone.wav)"
curl -sS --location 'http://127.0.0.1:8010/memories/add' \
--header 'Content-Type: application/json' \
--data "{
\"user_id\": \"${USER_ID}\",
\"user_key\": \"${USER_KEY}\",
\"session_id\": \"${SESSION_ID}\",
\"app_id\": \"default\",
\"project_id\": \"default\",
\"messages\": [
{
\"sender_id\": \"${USER_ID}\",
\"role\": \"user\",
\"timestamp\": ${TIMESTAMP_MS},
\"content\": [
{
\"type\": \"text\",
\"text\": \"请通过 Memory Gateway 同时记住这段文字、音频和图片:图片里有左侧红色方块、右侧蓝色圆形、底部绿色横条;音频是一段短促的测试音。以后可能会问图片中各个物体的位置和颜色。\"
},
{
\"type\": \"audio\",
\"base64\": \"${AUDIO_BASE64}\",
\"ext\": \"wav\",
\"name\": \"simple-tone.wav\"
},
{
\"type\": \"image\",
\"uri\": \"file:///home/tom/memory-gateway/tests/simple-multimodal-image.png\",
\"ext\": \"png\",
\"name\": \"simple-multimodal-image.png\"
}
]
}
]
}"
curl -sS -X POST http://127.0.0.1:8010/memories/add/multipart \
-F 'user_id=gateway_demo_user' \
-F 'user_key=<USER_KEY>' \
-F 'session_id=chat:gateway_demo_conversation' \
-F 'app_id=default' \
-F 'project_id=default' \
-F 'messages=[
{
"sender_id": "gateway_demo_user",
"role": "user",
"timestamp": 1782111275810,
"content": [
{
"type": "text",
"text": "请记住这次上传:图片里有左上红色方块、右上蓝色圆形、底部绿色横条;音频是一段短促测试音。"
},
{
"type": "image",
"upload_id": "image_1",
"name": "simple-multimodal-image.png",
"ext": "png"
},
{
"type": "audio",
"upload_id": "audio_1",
"name": "simple-tone.wav",
"ext": "wav"
}
]
}
]' \
-F 'image_1=@tests/simple-multimodal-image.png;type=image/png' \
-F 'audio_1=@tests/simple-tone.wav;type=audio/wav'
```
Response:
Expected shape:
```json
{
"session_id": "chat:gateway-multimodal-20260611180257",
"session_id": "chat:gateway_demo_conversation",
"backend": {
"request_id": "c9e24b8d27ee4ad08a8df70273336637",
"request_id": "0d6451f4077040e4af207cc6b034ea34",
"data": {
"message_count": 1,
"status": "accumulated"
@ -147,308 +129,153 @@ Response:
}
```
HTTP metadata:
Gateway stores the uploaded files and forwards upstream-compatible `base64` or
`text` content. The client does not send `file://` and does not send base64.
```text
HTTP_STATUS:200
TOTAL_TIME:1.552665
```
Common errors:
## 3. Flush through Gateway
- Missing file field for an `upload_id`: `422`
- Duplicate `upload_id`: `422`
- Extra uploaded file field not referenced by `messages`: `422`
- Unsupported MIME type: `415`
- File too large: `413`
Request:
## 4. Flush chat session
`/memories/add/multipart` only appends messages. Call flush when the session
should be extracted and indexed.
```bash
curl -sS --location 'http://127.0.0.1:8010/memories/flush' \
--header 'Content-Type: application/json' \
--data "{
\"user_id\": \"${USER_ID}\",
\"user_key\": \"${USER_KEY}\",
\"session_id\": \"${SESSION_ID}\",
\"app_id\": \"default\",
\"project_id\": \"default\"
}"
curl -sS -X POST http://127.0.0.1:8010/memories/flush \
-H 'Content-Type: application/json' \
-d '{
"user_id": "gateway_demo_user",
"user_key": "<USER_KEY>",
"session_id": "chat:gateway_demo_conversation",
"app_id": "default",
"project_id": "default"
}'
```
Response:
Expected shape:
```json
{
"session_id": "chat:gateway-multimodal-20260611180257",
"session_id": "chat:gateway_demo_conversation",
"backend": {
"request_id": "8eb7d5db2d3b43f4999f445aabb813b1",
"data": {
"status": "extracted"
}
"request_id": "4df5415115a34f109c564abd2f9012c6",
"data": {"status": "extracted"}
}
}
```
HTTP metadata:
```text
HTTP_STATUS:200
TOTAL_TIME:2.135721
```
## 4. Search through Gateway
upstream memory service indexing can lag briefly after `flush`, so this test waited about 2 seconds before searching.
Request:
## 5. Search chat session
```bash
sleep 2
curl -sS --location 'http://127.0.0.1:8010/memories/search' \
--header 'Content-Type: application/json' \
--data "{
\"user_id\": \"${USER_ID}\",
\"user_key\": \"${USER_KEY}\",
\"conversation_id\": \"${CONVERSATION_ID}\",
\"query\": \"图片里的蓝色圆形在哪里?音频是什么?\",
\"scope\": [\"current_chat\"],
\"top_k\": 5,
\"app_id\": \"default\",
\"project_id\": \"default\"
}"
curl -sS -X POST http://127.0.0.1:8010/memories/search \
-H 'Content-Type: application/json' \
-d '{
"user_id": "gateway_demo_user",
"user_key": "<USER_KEY>",
"conversation_id": "gateway_demo_conversation",
"query": "图片里的蓝色圆形在哪里?底部是什么颜色的横条?",
"scope": ["current_chat"],
"top_k": 5,
"app_id": "default",
"project_id": "default"
}'
```
Response:
Expected result excerpt:
```json
{
"results": [
{
"id": "gateway_user_20260611180257_ep_20260611_00000001",
"session_id": "chat:gateway-multimodal-20260611180257",
"text": "On June 11, 2026 at 10:02 AM UTC, user gateway_user_20260611180257 uploaded a multimodal memory package via Memory Gateway. The package included an image file named simple-multimodal-image.png and a short test audio clip. The image displayed three geometric shapes on a light gray background: a solid red square in the upper-left, a solid blue circle in the upper-right (horizontally aligned with the square), and a long, thin green horizontal rectangle spanning the bottom below both shapes. The user instructed the system to retain these details, anticipating future queries regarding the objects' positions and colors.",
"score": 0.6069304347038269,
"session_id": "chat:gateway_demo_conversation",
"source_scope": "current_chat",
"resource_id": null,
"resource_uri": null,
"raw": {
"id": "gateway_user_20260611180257_ep_20260611_00000001",
"user_id": "gateway_user_20260611180257",
"app_id": "default",
"project_id": "default",
"session_id": "chat:gateway-multimodal-20260611180257",
"timestamp": "2026-06-11T10:02:57Z",
"sender_ids": [
"gateway_user_20260611180257"
],
"summary": "On June 11, 2026 at 10:02 AM UTC, user gateway_user_20260611180257 uploaded a multimodal memory package via Memory Gateway. The package included an image file named simple-multimodal-image.png and a s",
"subject": "gateway_user_20260611180257 Multimodal Memory Upload June 11, 2026",
"episode": "On June 11, 2026 at 10:02 AM UTC, user gateway_user_20260611180257 uploaded a multimodal memory package via Memory Gateway. The package included an image file named simple-multimodal-image.png and a short test audio clip. The image displayed three geometric shapes on a light gray background: a solid red square in the upper-left, a solid blue circle in the upper-right (horizontally aligned with the square), and a long, thin green horizontal rectangle spanning the bottom below both shapes. The user instructed the system to retain these details, anticipating future queries regarding the objects' positions and colors.",
"type": "Conversation",
"score": 0.6069304347038269,
"atomic_facts": [
{
"id": "gateway_user_20260611180257_af_20260611_00000004",
"content": "gateway_user_20260611180257 stated that questions about the positions and colors of the objects in the image might be asked in the future.",
"score": 0.6069304347038269
}
]
}
"text": "The image contained a red square, a blue circle, and a green horizontal rectangle.",
"attachments": [
{
"type": "image",
"name": "simple-multimodal-image.png",
"internal_uri": "file:///home/tom/memory-gateway/data/storage/..."
},
{
"type": "audio",
"name": "simple-tone.wav",
"internal_uri": "file:///home/tom/memory-gateway/data/storage/..."
}
]
}
]
}
```
HTTP metadata:
## 6. Upload an independent resource
```text
HTTP_STATUS:200
TOTAL_TIME:0.064128
```
# Other Memory Gateway API tests
The following calls used a temporary Gateway database and storage directory. All requests target Memory Gateway at `http://127.0.0.1:8010`.
## 5. Health
Request:
Use `/resources` when the file is an independent resource, not just an
attachment inside one chat message.
```bash
curl -sS --location 'http://127.0.0.1:8010/health'
curl -sS -X POST http://127.0.0.1:8010/resources \
-F 'user_id=gateway_demo_user' \
-F 'user_key=<USER_KEY>' \
-F 'app_id=default' \
-F 'project_id=default' \
-F 'title=Gateway demo image resource' \
-F 'description=Demo upload for simple multimodal image' \
-F 'file=@tests/simple-multimodal-image.png;type=image/png'
```
Response:
Expected shape:
```json
{
"status": "ok",
"api": {"status": "ok"},
"backend": {
"status": "ok",
"base_url": "http://10.6.80.123:1995",
"data": {"status": "ok"}
}
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.034914
```
## 6. Invalid credentials
Request:
```bash
curl -sS --location \
'http://127.0.0.1:8010/resources?user_id=other_api_20260612095541&user_key=wrong-key'
```
Response:
```json
{"detail":"invalid user credentials"}
```
```text
HTTP_STATUS:401
TOTAL_TIME:0.001447
```
## 7. Upload resource
The temporary test user was created with:
```bash
curl -sS --location 'http://127.0.0.1:8010/users' \
--header 'Content-Type: application/json' \
--data '{"user_id":"other_api_20260612095541"}'
```
User response:
```json
{
"user_id": "other_api_20260612095541",
"user_key": "uk_REDACTED",
"created_at": "2026-06-12T01:55:41.448076+00:00"
}
```
Upload request:
```bash
cd /home/tom/memory-gateway
curl -sS --location 'http://127.0.0.1:8010/resources' \
--form 'user_id=other_api_20260612095541' \
--form 'user_key=uk_REDACTED' \
--form 'app_id=default' \
--form 'project_id=default' \
--form 'title=Gateway API image resource' \
--form 'description=Resource lifecycle test through Memory Gateway' \
--form 'file=@tests/simple-multimodal-image.png;type=image/png'
```
Response:
```json
{
"resource_id": "r_2700e435f72a49e6a7f736d17f8c7ac7",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"uri": "resource://other_api_20260612095541/r_2700e435f72a49e6a7f736d17f8c7ac7",
"resource_id": "r_1678eacf3e8c49f9a8863454c5b35e68",
"session_id": "resource:gateway_demo_user:r_1678eacf3e8c49f9a8863454c5b35e68",
"uri": "resource://gateway_demo_user/r_1678eacf3e8c49f9a8863454c5b35e68",
"status": "extracted"
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:4.700296
```
Unlike `/memories/add/multipart`, `/resources` automatically calls upstream add
and flush.
## 8. List resources
Request:
## 7. List resources
```bash
curl -sS --location \
'http://127.0.0.1:8010/resources?user_id=other_api_20260612095541&user_key=uk_REDACTED'
curl -sS \
'http://127.0.0.1:8010/resources?user_id=gateway_demo_user&user_key=<USER_KEY>'
```
Response:
Expected shape:
```json
{
"resources": [
{
"resource_id": "r_2700e435f72a49e6a7f736d17f8c7ac7",
"user_id": "other_api_20260612095541",
"resource_id": "r_1678eacf3e8c49f9a8863454c5b35e68",
"filename": "simple-multimodal-image.png",
"content_type": "image",
"mime_type": "image/png",
"uri": "resource://other_api_20260612095541/r_2700e435f72a49e6a7f736d17f8c7ac7",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"status": "extracted",
"title": "Gateway API image resource",
"description": "Resource lifecycle test through Memory Gateway",
"created_at": "2026-06-12T01:55:41.527716+00:00",
"updated_at": "2026-06-12T01:55:46.204082+00:00"
"uri": "resource://gateway_demo_user/r_1678eacf3e8c49f9a8863454c5b35e68",
"session_id": "resource:gateway_demo_user:r_1678eacf3e8c49f9a8863454c5b35e68",
"status": "extracted"
}
]
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.001785
```
## 9. Resource detail
Request:
## 8. Search resources
```bash
curl -sS --location \
'http://127.0.0.1:8010/resources/r_2700e435f72a49e6a7f736d17f8c7ac7?user_id=other_api_20260612095541&user_key=uk_REDACTED'
```
Response:
```json
{
"resources": [
{
"resource_id": "r_2700e435f72a49e6a7f736d17f8c7ac7",
"user_id": "other_api_20260612095541",
"filename": "simple-multimodal-image.png",
"content_type": "image",
"mime_type": "image/png",
"uri": "resource://other_api_20260612095541/r_2700e435f72a49e6a7f736d17f8c7ac7",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"status": "extracted",
"title": "Gateway API image resource",
"description": "Resource lifecycle test through Memory Gateway",
"created_at": "2026-06-12T01:55:41.527716+00:00",
"updated_at": "2026-06-12T01:55:46.204082+00:00"
}
]
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.001634
```
## 10. Search resource memory
Request:
```bash
curl -sS --location 'http://127.0.0.1:8010/memories/search' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "other_api_20260612095541",
"user_key": "uk_REDACTED",
"query": "图片中有哪些颜色和形状?",
curl -sS -X POST http://127.0.0.1:8010/memories/search \
-H 'Content-Type: application/json' \
-d '{
"user_id": "gateway_demo_user",
"user_key": "<USER_KEY>",
"query": "这张资源图片里有哪些几何图形和颜色?",
"scope": ["resources"],
"top_k": 5,
"app_id": "default",
@ -456,251 +283,43 @@ curl -sS --location 'http://127.0.0.1:8010/memories/search' \
}'
```
Response:
Expected result excerpt:
```json
{
"results": [
{
"id": "other_api_20260612095541_ep_20260612_00000001",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"text": "On June 12, 2026 at 01:55 AM UTC, the user other_api_20260612095541 uploaded an image titled 'simple-multimodal-image.png' for visual analysis. The image displayed three distinct geometric shapes on a plain, light gray background. The composition included a solid red square in the upper-left portion, a solid blue circle in the upper-right portion, and a long, thin, horizontal green rectangle situated below both shapes. The red square and blue circle were roughly aligned horizontally, while the green rectangle spanned a width greater than either of the upper shapes.",
"score": 0.6418947577476501,
"source_scope": "resources",
"resource_id": "r_2700e435f72a49e6a7f736d17f8c7ac7",
"resource_uri": "resource://other_api_20260612095541/r_2700e435f72a49e6a7f736d17f8c7ac7",
"raw": {
"id": "other_api_20260612095541_ep_20260612_00000001",
"user_id": "other_api_20260612095541",
"app_id": "default",
"project_id": "default",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"timestamp": "2026-06-12T01:55:41.541000Z",
"sender_ids": ["other_api_20260612095541"],
"summary": "On June 12, 2026 at 01:55 AM UTC, the user other_api_20260612095541 uploaded an image titled 'simple-multimodal-image.png' for visual analysis. The image displayed three distinct geometric shapes on a",
"subject": "Visual Analysis of Geometric Shapes Uploaded by other_api_20260612095541 on June 12, 2026",
"episode": "On June 12, 2026 at 01:55 AM UTC, the user other_api_20260612095541 uploaded an image titled 'simple-multimodal-image.png' for visual analysis. The image displayed three distinct geometric shapes on a plain, light gray background. The composition included a solid red square in the upper-left portion, a solid blue circle in the upper-right portion, and a long, thin, horizontal green rectangle situated below both shapes. The red square and blue circle were roughly aligned horizontally, while the green rectangle spanned a width greater than either of the upper shapes.",
"type": "Conversation",
"score": 0.6418947577476501,
"atomic_facts": [
{
"id": "other_api_20260612095541_af_20260612_00000001",
"content": "The image displays three distinct geometric shapes on a plain, light gray background.",
"score": 0.6418947577476501
}
]
}
"resource_id": "r_1678eacf3e8c49f9a8863454c5b35e68",
"resource_uri": "resource://gateway_demo_user/r_1678eacf3e8c49f9a8863454c5b35e68",
"text": "The image displayed a red square, a blue circle, and a green rectangle.",
"attachments": [
{
"type": "image",
"name": "simple-multimodal-image.png",
"internal_uri": "file:///home/tom/memory-gateway/data/storage/..."
}
]
}
]
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.176981
```
## Multipart vs resources
## 11. Override memory
Use `/memories/add/multipart` when the upload belongs to a chat/session message:
Request:
- caller supplies `session_id`, usually `chat:{conversation_id}`;
- caller defines `upload_id` values in `messages`;
- caller uploads files as form fields with names matching `upload_id`;
- Gateway only calls upstream add;
- caller should call `/memories/flush`;
- search normally uses `current_chat` or `all_user_memory`.
```bash
curl -sS --location --request PATCH \
'http://127.0.0.1:8010/memories/other_api_20260612095541_ep_20260612_00000001' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "other_api_20260612095541",
"user_key": "uk_REDACTED",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"override_text": "OVERRIDE: 图片左侧是红色方块,右侧是蓝色圆形,底部是绿色横条。"
}'
```
Use `/resources` when the upload is an independent resource:
Response:
```json
{
"memory_id": "other_api_20260612095541_ep_20260612_00000001",
"override_id": "o_328f03b40b164c4896640fd2567042cb",
"status": "active"
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.007037
```
The next search returned the overridden text:
Request:
```bash
curl -sS --location 'http://127.0.0.1:8010/memories/search' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "other_api_20260612095541",
"user_key": "uk_REDACTED",
"query": "图片中有哪些颜色和形状?",
"scope": ["resources"],
"top_k": 5,
"app_id": "default",
"project_id": "default"
}'
```
```json
{
"results": [
{
"id": "other_api_20260612095541_ep_20260612_00000001",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"text": "OVERRIDE: 图片左侧是红色方块,右侧是蓝色圆形,底部是绿色横条。",
"score": 0.6418947577476501,
"source_scope": "resources",
"resource_id": "r_2700e435f72a49e6a7f736d17f8c7ac7",
"resource_uri": "resource://other_api_20260612095541/r_2700e435f72a49e6a7f736d17f8c7ac7",
"raw": {
"id": "other_api_20260612095541_ep_20260612_00000001",
"user_id": "other_api_20260612095541",
"app_id": "default",
"project_id": "default",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"timestamp": "2026-06-12T01:55:41.541000Z",
"sender_ids": ["other_api_20260612095541"],
"summary": "On June 12, 2026 at 01:55 AM UTC, the user other_api_20260612095541 uploaded an image titled 'simple-multimodal-image.png' for visual analysis. The image displayed three distinct geometric shapes on a",
"subject": "Visual Analysis of Geometric Shapes Uploaded by other_api_20260612095541 on June 12, 2026",
"episode": "On June 12, 2026 at 01:55 AM UTC, the user other_api_20260612095541 uploaded an image titled 'simple-multimodal-image.png' for visual analysis. The image displayed three distinct geometric shapes on a plain, light gray background. The composition included a solid red square in the upper-left portion, a solid blue circle in the upper-right portion, and a long, thin, horizontal green rectangle situated below both shapes. The red square and blue circle were roughly aligned horizontally, while the green rectangle spanned a width greater than either of the upper shapes.",
"type": "Conversation",
"score": 0.6418947577476501,
"atomic_facts": [
{
"id": "other_api_20260612095541_af_20260612_00000001",
"content": "The image displays three distinct geometric shapes on a plain, light gray background.",
"score": 0.6418947577476501
}
]
},
"override_id": "o_328f03b40b164c4896640fd2567042cb"
}
]
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.055485
```
## 12. Delete memory with tombstone
Request:
```bash
curl -sS --location --request DELETE \
'http://127.0.0.1:8010/memories/other_api_20260612095541_ep_20260612_00000001' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "other_api_20260612095541",
"user_key": "uk_REDACTED",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"reason": "Gateway API tombstone test"
}'
```
Response:
```json
{
"memory_id": "other_api_20260612095541_ep_20260612_00000001",
"tombstone_id": "t_2cba49bf3b6641ea96865612deebc036",
"status": "deleted"
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.006502
```
Repeating the resource search after creating the tombstone:
```bash
curl -sS --location 'http://127.0.0.1:8010/memories/search' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "other_api_20260612095541",
"user_key": "uk_REDACTED",
"query": "图片中有哪些颜色和形状?",
"scope": ["resources"],
"top_k": 5,
"app_id": "default",
"project_id": "default"
}'
```
```json
{"results":[]}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.067841
```
## 13. Delete resource
Request:
```bash
curl -sS --location --request DELETE \
'http://127.0.0.1:8010/resources/r_2700e435f72a49e6a7f736d17f8c7ac7?user_id=other_api_20260612095541&user_key=uk_REDACTED'
```
Response:
```json
{
"resource_id": "r_2700e435f72a49e6a7f736d17f8c7ac7",
"session_id": "resource:other_api_20260612095541:r_2700e435f72a49e6a7f736d17f8c7ac7",
"uri": "resource://other_api_20260612095541/r_2700e435f72a49e6a7f736d17f8c7ac7",
"status": "deleted"
}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.014089
```
List after deletion:
```bash
curl -sS --location \
'http://127.0.0.1:8010/resources?user_id=other_api_20260612095541&user_key=uk_REDACTED'
```
```json
{"resources":[]}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.001226
```
Detail after deletion:
```bash
curl -sS --location \
'http://127.0.0.1:8010/resources/r_2700e435f72a49e6a7f736d17f8c7ac7?user_id=other_api_20260612095541&user_key=uk_REDACTED'
```
```json
{"resources":[]}
```
```text
HTTP_STATUS:200
TOTAL_TIME:0.001223
```
- Gateway creates `resource_id`;
- Gateway creates `session_id = resource:{user_id}:{resource_id}`;
- Gateway writes `user_resources`;
- Gateway automatically calls upstream add and flush;
- search normally uses `resources`.

View File

@ -897,6 +897,117 @@ async def test_add_memory_materializes_base64_attachment(
assert backend.add_calls[0]["messages"][0]["content"][0]["base64"] == encoded
@pytest.mark.asyncio
async def test_add_memory_multipart_uploads_files_to_chat_session(
config: GatewayConfig,
repo: MemoryRepository,
) -> None:
backend = FakeBackendClient()
messages = [
{
"sender_id": "u_123",
"role": "user",
"timestamp": 1234567890123,
"content": [
{"type": "text", "text": "remember these attachments"},
{
"type": "image",
"upload_id": "image_1",
"name": "picture.png",
"ext": "png",
},
{
"type": "audio",
"upload_id": "audio_1",
"name": "tone.wav",
"ext": "wav",
},
],
}
]
async with app_client(config, backend) as client:
user_key = await create_user(client)
response = await client.post(
"/memories/add/multipart",
data={
"user_id": "u_123",
"user_key": user_key,
"session_id": "chat:c_uploads",
"app_id": "default",
"project_id": "default",
"messages": json.dumps(messages),
},
files=[
("image_1", ("picture.png", b"png bytes", "image/png")),
("audio_1", ("tone.wav", b"wav bytes", "audio/wav")),
],
)
assert response.status_code == 200, response.text
assert response.json() == {
"session_id": "chat:c_uploads",
"backend": {"request_id": "add", "data": {"status": "accumulated"}},
}
content = backend.add_calls[0]["messages"][0]["content"]
assert content == [
{"type": "text", "text": "remember these attachments"},
{
"type": "image",
"name": "picture.png",
"ext": "png",
"base64": base64.b64encode(b"png bytes").decode("ascii"),
},
{
"type": "audio",
"name": "tone.wav",
"ext": "wav",
"base64": base64.b64encode(b"wav bytes").decode("ascii"),
},
]
attachments = repo.list_attachments_for_session("u_123", "chat:c_uploads")
assert [(item["name"], item["source"]) for item in attachments] == [
("picture.png", "memory_add_upload"),
("tone.wav", "memory_add_upload"),
]
for attachment in attachments:
path = Path(attachment["internal_uri"].removeprefix("file://"))
assert path.exists()
@pytest.mark.asyncio
async def test_add_memory_multipart_rejects_missing_upload_file(
config: GatewayConfig,
) -> None:
backend = FakeBackendClient()
messages = [
{
"sender_id": "u_123",
"role": "user",
"timestamp": 1234567890123,
"content": [
{"type": "image", "upload_id": "image_1", "name": "picture.png"}
],
}
]
async with app_client(config, backend) as client:
user_key = await create_user(client)
response = await client.post(
"/memories/add/multipart",
data={
"user_id": "u_123",
"user_key": user_key,
"session_id": "chat:c_missing_upload",
"messages": json.dumps(messages),
},
)
assert response.status_code == 422
assert "missing upload file for upload_id: image_1" in response.text
assert backend.add_calls == []
@pytest.mark.asyncio
async def test_add_memory_deduplicates_retried_base64_attachment(
config: GatewayConfig,