add multimodal memory proxy and API logging
This commit is contained in:
BIN
tests/simple-multimodal-image.png
Normal file
BIN
tests/simple-multimodal-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 259 B |
BIN
tests/simple-tone.wav
Normal file
BIN
tests/simple-tone.wav
Normal file
Binary file not shown.
1
tests/test.md
Normal file
1
tests/test.md
Normal file
@ -0,0 +1 @@
|
||||
这是测试文件
|
||||
705
tests/test_command.md
Normal file
705
tests/test_command.md
Normal file
@ -0,0 +1,705 @@
|
||||
# Memory Gateway multimodal API test
|
||||
|
||||
This file records a real end-to-end test through **Memory Gateway**, not direct EverOS calls.
|
||||
|
||||
Gateway URL used by curl:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8010
|
||||
```
|
||||
|
||||
Gateway upstream EverOS:
|
||||
|
||||
```text
|
||||
http://10.6.80.123:1995
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
cd /home/tom/memory-gateway
|
||||
|
||||
EVEROS_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
|
||||
```
|
||||
|
||||
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
|
||||
curl -sS --location 'http://127.0.0.1:8010/users' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"user_id": "gateway_user_20260611180257"
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "gateway_user_20260611180257",
|
||||
"user_key": "uk_REDACTED",
|
||||
"created_at": "2026-06-11T10:02:57.435437+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
HTTP metadata:
|
||||
|
||||
```text
|
||||
HTTP_STATUS:200
|
||||
TOTAL_TIME:0.022431
|
||||
```
|
||||
|
||||
## 2. Add text + audio(base64) + image(file) through Gateway
|
||||
|
||||
Request:
|
||||
|
||||
```bash
|
||||
cd /home/tom/memory-gateway
|
||||
|
||||
USER_ID="gateway_user_20260611180257"
|
||||
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\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "chat:gateway-multimodal-20260611180257",
|
||||
"everos": {
|
||||
"request_id": "c9e24b8d27ee4ad08a8df70273336637",
|
||||
"data": {
|
||||
"message_count": 1,
|
||||
"status": "accumulated"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HTTP metadata:
|
||||
|
||||
```text
|
||||
HTTP_STATUS:200
|
||||
TOTAL_TIME:1.552665
|
||||
```
|
||||
|
||||
## 3. Flush through Gateway
|
||||
|
||||
Request:
|
||||
|
||||
```bash
|
||||
curl -sS --location 'http://127.0.0.1:8010/memories/flush' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"user_id": "gateway_user_20260611180257",
|
||||
"user_key": "uk_REDACTED",
|
||||
"session_id": "chat:gateway-multimodal-20260611180257",
|
||||
"app_id": "default",
|
||||
"project_id": "default"
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "chat:gateway-multimodal-20260611180257",
|
||||
"everos": {
|
||||
"request_id": "8eb7d5db2d3b43f4999f445aabb813b1",
|
||||
"data": {
|
||||
"status": "extracted"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HTTP metadata:
|
||||
|
||||
```text
|
||||
HTTP_STATUS:200
|
||||
TOTAL_TIME:2.135721
|
||||
```
|
||||
|
||||
## 4. Search through Gateway
|
||||
|
||||
EverOS indexing can lag briefly after `flush`, so this test waited about 2 seconds before searching.
|
||||
|
||||
Request:
|
||||
|
||||
```bash
|
||||
sleep 2
|
||||
|
||||
curl -sS --location 'http://127.0.0.1:8010/memories/search' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"user_id": "gateway_user_20260611180257",
|
||||
"user_key": "uk_REDACTED",
|
||||
"conversation_id": "gateway-multimodal-20260611180257",
|
||||
"query": "图片里的蓝色圆形在哪里?音频是什么?",
|
||||
"scope": ["current_chat"],
|
||||
"top_k": 5,
|
||||
"app_id": "default",
|
||||
"project_id": "default"
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```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,
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
HTTP metadata:
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
curl -sS --location 'http://127.0.0.1:8010/health'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"api": {"status": "ok"},
|
||||
"everos": {
|
||||
"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",
|
||||
"status": "extracted"
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
HTTP_STATUS:200
|
||||
TOTAL_TIME:4.700296
|
||||
```
|
||||
|
||||
## 8. List resources
|
||||
|
||||
Request:
|
||||
|
||||
```bash
|
||||
curl -sS --location \
|
||||
'http://127.0.0.1:8010/resources?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.001785
|
||||
```
|
||||
|
||||
## 9. Resource detail
|
||||
|
||||
Request:
|
||||
|
||||
```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": "图片中有哪些颜色和形状?",
|
||||
"scope": ["resources"],
|
||||
"top_k": 5,
|
||||
"app_id": "default",
|
||||
"project_id": "default"
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
HTTP_STATUS:200
|
||||
TOTAL_TIME:0.176981
|
||||
```
|
||||
|
||||
## 11. Override memory
|
||||
|
||||
Request:
|
||||
|
||||
```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: 图片左侧是红色方块,右侧是蓝色圆形,底部是绿色横条。"
|
||||
}'
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
80
tests/test_everos_integration.py
Normal file
80
tests/test_everos_integration.py
Normal file
@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from core.api import create_app
|
||||
from core.config import GatewayConfig
|
||||
from core.everos_client import EverOSClient
|
||||
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
def _integration_enabled() -> bool:
|
||||
return os.environ.get("RUN_EVEROS_INTEGRATION") == "1"
|
||||
|
||||
|
||||
def _ingest_integration_enabled() -> bool:
|
||||
return os.environ.get("RUN_EVEROS_INGEST_INTEGRATION") == "1"
|
||||
|
||||
|
||||
def _everos_base_url() -> str:
|
||||
return os.environ.get("EVEROS_BASE_URL", "http://127.0.0.1:1995")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _integration_enabled(),
|
||||
reason="set RUN_EVEROS_INTEGRATION=1 to run against a real EverOS service",
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_everos_health_check() -> None:
|
||||
client = EverOSClient(_everos_base_url(), timeout=10)
|
||||
|
||||
health = await client.health_check()
|
||||
|
||||
assert isinstance(health, dict)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _ingest_integration_enabled(),
|
||||
reason=(
|
||||
"set RUN_EVEROS_INGEST_INTEGRATION=1 to run real EverOS add/flush ingestion"
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_gateway_uploads_text_resource_to_real_everos(tmp_path: Path) -> None:
|
||||
config = GatewayConfig(
|
||||
everos_base_url=_everos_base_url(),
|
||||
database_path=tmp_path / "gateway.sqlite3",
|
||||
storage_dir=tmp_path / "storage",
|
||||
everos_ingest_attempts=1,
|
||||
everos_timeout_seconds=30,
|
||||
)
|
||||
app = create_app(config=config)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
user_id = f"it_{uuid4().hex}"
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
created_user = await client.post("/users", json={"user_id": user_id})
|
||||
assert created_user.status_code == 200, created_user.text
|
||||
user_key = created_user.json()["user_key"]
|
||||
|
||||
uploaded = await client.post(
|
||||
"/resources",
|
||||
data={"user_id": user_id, "user_key": user_key},
|
||||
files={
|
||||
"file": (
|
||||
"real-everos.txt",
|
||||
b"real everos integration",
|
||||
"text/plain",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
assert uploaded.status_code == 200, uploaded.text
|
||||
assert uploaded.json()["status"] == "extracted"
|
||||
@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@ -10,6 +13,8 @@ from core.api import create_app
|
||||
from core.config import GatewayConfig
|
||||
from core.db import init_db
|
||||
from core.repository import MemoryRepository
|
||||
import core.api as api_module
|
||||
import core.service as service_module
|
||||
|
||||
|
||||
class FakeEverOSClient:
|
||||
@ -131,6 +136,93 @@ def test_create_app_uses_configured_everos_timeout(config: GatewayConfig) -> Non
|
||||
assert app.state.everos_client.timeout == 7.5
|
||||
|
||||
|
||||
def test_create_app_uses_project_name(config: GatewayConfig) -> None:
|
||||
app = create_app(config=config, everos_client=FakeEverOSClient())
|
||||
|
||||
assert app.title == "memory-gateway"
|
||||
|
||||
|
||||
def test_api_log_body_capture_policy_skips_large_and_multipart_requests() -> None:
|
||||
assert api_module._should_capture_request_body("application/json", 100)
|
||||
assert not api_module._should_capture_request_body(
|
||||
"application/json",
|
||||
api_module.MAX_LOG_BODY_BYTES + 1,
|
||||
)
|
||||
assert not api_module._should_capture_request_body(
|
||||
"multipart/form-data; boundary=test",
|
||||
100,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_logs_request_time_address_input_and_output(
|
||||
config: GatewayConfig,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
caplog.set_level(logging.INFO, logger="memory_gateway.api")
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
user_key = await create_user(client, "u_log")
|
||||
response = await client.get(
|
||||
"/resources",
|
||||
params={"user_id": "u_log", "user_key": user_key},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
events = [json.loads(record.message) for record in caplog.records]
|
||||
create_user_event = next(item for item in events if item["path"] == "/users")
|
||||
assert create_user_event["output"]["body"]["user_key"] == "[REDACTED]"
|
||||
event = next(item for item in events if item["path"] == "/resources")
|
||||
assert event["request_time"]
|
||||
assert event["method"] == "GET"
|
||||
assert event["url"] == "http://test/resources?user_id=u_log&user_key=[REDACTED]"
|
||||
assert event["client"] == "127.0.0.1"
|
||||
assert event["duration_ms"] >= 0
|
||||
assert event["input"]["query_params"] == {
|
||||
"user_id": "u_log",
|
||||
"user_key": "[REDACTED]",
|
||||
}
|
||||
assert event["output"]["status_code"] == 200
|
||||
assert event["output"]["body"] == {"resources": []}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_logs_do_not_expose_secrets_from_large_json_bodies(
|
||||
config: GatewayConfig,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
caplog.set_level(logging.INFO, logger="memory_gateway.api")
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
user_key = await create_user(client, "u_large_log")
|
||||
caplog.clear()
|
||||
response = await client.post(
|
||||
"/memories/add",
|
||||
json={
|
||||
"user_id": "u_large_log",
|
||||
"user_key": user_key,
|
||||
"session_id": "chat:c_large_log",
|
||||
"messages": [
|
||||
{
|
||||
"sender_id": "u_large_log",
|
||||
"role": "user",
|
||||
"timestamp": 1234567890123,
|
||||
"content": "x" * 5000,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
event = next(
|
||||
json.loads(record.message)
|
||||
for record in caplog.records
|
||||
if json.loads(record.message)["path"] == "/memories/add"
|
||||
)
|
||||
assert event["input"]["body"]["truncated"] is True
|
||||
assert user_key not in json.dumps(event, ensure_ascii=False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_reports_api_and_everos_ok(
|
||||
config: GatewayConfig,
|
||||
@ -217,7 +309,8 @@ async def test_upload_resource_creates_record_and_calls_everos(
|
||||
assert add_payload["session_id"] == f"resource:u_123:{resource_id}"
|
||||
content = add_payload["messages"][0]["content"][0]
|
||||
assert content["type"] == "text"
|
||||
assert content["uri"].startswith("file://")
|
||||
assert content["text"] == "pay in 30 days"
|
||||
assert "uri" not in content
|
||||
assert content["extras"] == {"resource_id": resource_id, "source": "user_upload"}
|
||||
assert everos.flush_calls == [
|
||||
{
|
||||
@ -228,6 +321,52 @@ async def test_upload_resource_creates_record_and_calls_everos(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_binary_resource_sends_base64_content_to_everos(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
data={"user_id": "u_123", "user_key": user_key},
|
||||
files={"file": ("paper.pdf", b"%PDF bytes", "application/pdf")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
content = everos.add_calls[0]["messages"][0]["content"][0]
|
||||
assert content["type"] == "pdf"
|
||||
assert content["base64"] == base64.b64encode(b"%PDF bytes").decode("ascii")
|
||||
assert content["ext"] == "pdf"
|
||||
assert content["name"] == "paper.pdf"
|
||||
assert "uri" not in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_resource_uses_current_timestamp(
|
||||
config: GatewayConfig,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
service_module,
|
||||
"current_timestamp_ms",
|
||||
lambda: 1234567890123,
|
||||
raising=False,
|
||||
)
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/resources",
|
||||
data={"user_id": "u_123", "user_key": user_key},
|
||||
files={"file": ("timed.txt", b"time me", "text/plain")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert everos.add_calls[0]["messages"][0]["timestamp"] == 1234567890123
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_retries_transient_everos_failure(
|
||||
config: GatewayConfig,
|
||||
@ -409,6 +548,97 @@ async def test_resource_api_rejects_invalid_user_key(
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_memory_forwards_multimodal_payload_to_everos(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
audio = base64.b64encode(b"wav bytes").decode("ascii")
|
||||
content = [
|
||||
{"type": "text", "text": "remember the picture and audio"},
|
||||
{"type": "audio", "base64": audio, "ext": "wav", "name": "tone.wav"},
|
||||
{
|
||||
"type": "image",
|
||||
"uri": "file:///home/tom/memory-gateway/tests/simple-multimodal-image.png",
|
||||
"ext": "png",
|
||||
"name": "simple-multimodal-image.png",
|
||||
},
|
||||
]
|
||||
async with app_client(config, everos) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/memories/add",
|
||||
json={
|
||||
"user_id": "u_123",
|
||||
"user_key": user_key,
|
||||
"session_id": "chat:c_multimodal",
|
||||
"app_id": "default",
|
||||
"project_id": "default",
|
||||
"messages": [
|
||||
{
|
||||
"sender_id": "u_123",
|
||||
"role": "user",
|
||||
"timestamp": 1234567890123,
|
||||
"content": content,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"session_id": "chat:c_multimodal",
|
||||
"everos": {"request_id": "add", "data": {"status": "accumulated"}},
|
||||
}
|
||||
assert everos.add_calls == [
|
||||
{
|
||||
"session_id": "chat:c_multimodal",
|
||||
"app_id": "default",
|
||||
"project_id": "default",
|
||||
"messages": [
|
||||
{
|
||||
"sender_id": "u_123",
|
||||
"role": "user",
|
||||
"timestamp": 1234567890123,
|
||||
"content": content,
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flush_memory_forwards_request_to_everos(
|
||||
config: GatewayConfig,
|
||||
) -> None:
|
||||
everos = FakeEverOSClient()
|
||||
async with app_client(config, everos) as client:
|
||||
user_key = await create_user(client)
|
||||
response = await client.post(
|
||||
"/memories/flush",
|
||||
json={
|
||||
"user_id": "u_123",
|
||||
"user_key": user_key,
|
||||
"session_id": "chat:c_multimodal",
|
||||
"app_id": "default",
|
||||
"project_id": "default",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"session_id": "chat:c_multimodal",
|
||||
"everos": {"request_id": "flush", "data": {"status": "extracted"}},
|
||||
}
|
||||
assert everos.flush_calls == [
|
||||
{
|
||||
"session_id": "chat:c_multimodal",
|
||||
"app_id": "default",
|
||||
"project_id": "default",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deleted_resource_is_excluded_from_resource_scope_search(
|
||||
config: GatewayConfig,
|
||||
|
||||
Reference in New Issue
Block a user