add multimodal memory proxy and API logging
This commit is contained in:
@ -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