188 lines
6.8 KiB
Python
Executable File
188 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""End-to-end ingestion test for Memory OS.
|
|
|
|
Verifies the full pipeline: enqueue → ARQ worker → embedding → Qdrant upsert.
|
|
|
|
Usage:
|
|
python3 scripts/test_ingestion.py
|
|
|
|
Environment:
|
|
REDIS_PASSWORD Redis password (required)
|
|
QDRANT_API_KEY Qdrant API key (default: "")
|
|
REDIS_HOST Redis host (default: localhost)
|
|
REDIS_PORT Redis port (default: 6379)
|
|
QDRANT_HOST Qdrant host (default: localhost)
|
|
QDRANT_PORT Qdrant port (default: 6333)
|
|
COLLECTION_NAME Qdrant collection (default: knowledge_base)
|
|
|
|
Returns exit code 0 on success, 1 on failure.
|
|
"""
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import uuid
|
|
|
|
|
|
# ── Config from env ──────────────────────────────────────────────────────────
|
|
REDIS_HOST = os.environ.get("REDIS_HOST", "localhost")
|
|
REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379"))
|
|
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "")
|
|
QDRANT_HOST = os.environ.get("QDRANT_HOST", "localhost")
|
|
QDRANT_PORT = int(os.environ.get("QDRANT_PORT", "6333"))
|
|
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", "")
|
|
COLLECTION_NAME = os.environ.get("COLLECTION_NAME", "knowledge_base")
|
|
TIMEOUT = 60 # seconds for ARQ job completion
|
|
POLL_INTERVAL = 2 # seconds between polls
|
|
|
|
|
|
def fail(msg: str) -> None:
|
|
print(f"❌ {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
def ok(msg: str) -> None:
|
|
print(f"✅ {msg}")
|
|
|
|
|
|
# ── Test document ────────────────────────────────────────────────────────────
|
|
TEST_ID = uuid.uuid4().hex[:8]
|
|
TEST_TEXT = (
|
|
f"Memory OS ingestion test document {TEST_ID}. "
|
|
"This file was automatically generated by test_ingestion.py "
|
|
"to verify the end-to-end pipeline: enqueue, worker, embedding, Qdrant upsert."
|
|
)
|
|
TEST_PATH = f"/wiki/raw/test/ingestion-test-{TEST_ID}.md"
|
|
|
|
|
|
async def main() -> None:
|
|
print(f"=== Memory OS Ingestion Test (doc: {TEST_ID}) ===")
|
|
print()
|
|
|
|
# ── 1. Create temp file for the worker to ingest ──────────────────────────
|
|
print("1. Creating test document...")
|
|
# The worker reads from the wiki volume mounted in Docker.
|
|
# Write to /tmp as a fallback; the worker path depends on volume config.
|
|
tmpdir = tempfile.mkdtemp(prefix="memoryos-test-")
|
|
test_file = os.path.join(tmpdir, f"ingestion-test-{TEST_ID}.md")
|
|
with open(test_file, "w") as f:
|
|
f.write(f"# Test {TEST_ID}\n\n{TEST_TEXT}\n")
|
|
ok(f"Created {test_file}")
|
|
|
|
# ── 2. Enqueue ARQ job ───────────────────────────────────────────────────
|
|
print("2. Enqueuing ARQ job...")
|
|
try:
|
|
from arq import create_pool
|
|
from arq.connections import RedisSettings
|
|
except ImportError:
|
|
fail("arq not installed — run: pip install arq")
|
|
|
|
redis_settings = RedisSettings(
|
|
host=REDIS_HOST,
|
|
port=REDIS_PORT,
|
|
password=REDIS_PASSWORD or None,
|
|
)
|
|
|
|
redis = await create_pool(redis_settings)
|
|
job = await redis.enqueue_job("process_wiki_file", TEST_PATH)
|
|
job_id = job.job_id
|
|
ok(f"Job {job_id} enqueued")
|
|
|
|
# ── 3. Wait for completion ───────────────────────────────────────────────
|
|
print(f"3. Waiting for worker (timeout={TIMEOUT}s)...")
|
|
deadline = time.monotonic() + TIMEOUT
|
|
result = None
|
|
while time.monotonic() < deadline:
|
|
job_info = await redis.get_job_result(job_id)
|
|
if job_info is not None:
|
|
result = job_info.result
|
|
if job_info.success:
|
|
ok(f"Job completed in {TIMEOUT - (deadline - time.monotonic()):.0f}s")
|
|
break
|
|
else:
|
|
fail(f"Job failed: {job_info.result}")
|
|
await asyncio.sleep(POLL_INTERVAL)
|
|
|
|
if result is None:
|
|
fail(f"Job timed out after {TIMEOUT}s — is the ARQ worker running?")
|
|
|
|
# ── 4. Search Qdrant for the ingested point ──────────────────────────────
|
|
print("4. Searching Qdrant for ingested point...")
|
|
try:
|
|
from qdrant_client import AsyncQdrantClient
|
|
from qdrant_client.models import Filter, FieldCondition, MatchText
|
|
except ImportError:
|
|
fail("qdrant-client not installed — run: pip install qdrant-client")
|
|
|
|
client = AsyncQdrantClient(
|
|
host=QDRANT_HOST,
|
|
port=QDRANT_PORT,
|
|
api_key=QDRANT_API_KEY or None,
|
|
https=False,
|
|
)
|
|
|
|
# Search by the unique test ID in the payload
|
|
points, _ = await client.scroll(
|
|
collection_name=COLLECTION_NAME,
|
|
scroll_filter=Filter(
|
|
must=[
|
|
FieldCondition(
|
|
key="source_file",
|
|
match=MatchText(text=TEST_PATH),
|
|
)
|
|
]
|
|
),
|
|
with_vectors=True,
|
|
limit=5,
|
|
)
|
|
|
|
if not points:
|
|
fail(
|
|
f"No points found for {TEST_PATH} in collection "
|
|
f"'{COLLECTION_NAME}' — ingestion may have succeeded "
|
|
"but the source_file path may differ from expected."
|
|
)
|
|
|
|
point = points[0]
|
|
point_id = point.id
|
|
ok(f"Found point {point_id}")
|
|
|
|
# ── 5. Verify dense vector dimensions ────────────────────────────────────
|
|
print("5. Verifying embedding dimensions...")
|
|
if "dense" not in point.vector:
|
|
fail("Point has no 'dense' vector — named vectors may not be configured")
|
|
|
|
dims = len(point.vector["dense"])
|
|
if dims != 4096:
|
|
fail(f"Expected 4096 dimensions, got {dims}")
|
|
|
|
ok(f"{dims} dimensions ✓")
|
|
|
|
# ── 6. Cleanup ───────────────────────────────────────────────────────────
|
|
print("6. Cleaning up test point...")
|
|
await client.delete(
|
|
collection_name=COLLECTION_NAME,
|
|
points_selector=[point_id],
|
|
)
|
|
ok(f"Deleted point {point_id}")
|
|
|
|
# Clean up local temp file
|
|
os.remove(test_file)
|
|
os.rmdir(tmpdir)
|
|
|
|
await client.close()
|
|
await redis.close()
|
|
|
|
# ── 7. Summary ───────────────────────────────────────────────────────────
|
|
print()
|
|
print("═" * 60)
|
|
ok("All checks passed — ingestion pipeline is operational")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|