add multimodal memory proxy and API logging

This commit is contained in:
2026-06-12 11:04:53 +08:00
parent 8afb460883
commit a29009dc07
12 changed files with 2229 additions and 33 deletions

View File

@ -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,