213 lines
7.7 KiB
Python
213 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""Live test: multimodal AOM with Gemini Flash Lite.
|
||
|
|
|
||
|
|
Tests: image processing, text file, PDF-like, and the two-tier architecture
|
||
|
|
(basic mode without key vs advanced mode with key).
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||
|
|
|
||
|
|
from adclaw.memory_agent.store import MemoryStore
|
||
|
|
from adclaw.memory_agent.embeddings import FakeEmbeddingPipeline
|
||
|
|
from adclaw.memory_agent.ingest import IngestAgent
|
||
|
|
from adclaw.memory_agent.multimodal import MultimodalProcessor, is_multimodal_file
|
||
|
|
from adclaw.memory_agent.query import QueryAgent
|
||
|
|
from adclaw.memory_agent.models import AOMConfig
|
||
|
|
|
||
|
|
# Real LLM for text extraction — set env vars before running
|
||
|
|
API_KEY_LLM = os.environ.get("QWEN_API_KEY", "")
|
||
|
|
LLM_URL = os.environ.get("QWEN_API_URL", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions")
|
||
|
|
LLM_MODEL = os.environ.get("QWEN_MODEL", "qwen-plus")
|
||
|
|
|
||
|
|
|
||
|
|
async def real_llm_caller(prompt: str) -> str:
|
||
|
|
async with httpx.AsyncClient(timeout=60) as client:
|
||
|
|
resp = await client.post(
|
||
|
|
LLM_URL,
|
||
|
|
headers={"Authorization": f"Bearer {API_KEY_LLM}", "Content-Type": "application/json"},
|
||
|
|
json={"model": LLM_MODEL, "messages": [{"role": "user", "content": prompt}], "temperature": 0.3, "max_tokens": 500},
|
||
|
|
)
|
||
|
|
resp.raise_for_status()
|
||
|
|
content = resp.json()["choices"][0]["message"]["content"]
|
||
|
|
if "<think>" in content and "</think>" in content:
|
||
|
|
content = content[content.index("</think>") + len("</think>"):].strip()
|
||
|
|
return content
|
||
|
|
|
||
|
|
|
||
|
|
def print_header(text):
|
||
|
|
print(f"\n{'='*60}\n {text}\n{'='*60}")
|
||
|
|
|
||
|
|
|
||
|
|
def print_ok(text):
|
||
|
|
print(f" [OK] {text}")
|
||
|
|
|
||
|
|
|
||
|
|
def print_fail(text):
|
||
|
|
print(f" [FAIL] {text}")
|
||
|
|
|
||
|
|
|
||
|
|
async def main():
|
||
|
|
# Check if Gemini key is provided as argument
|
||
|
|
gemini_key = sys.argv[1] if len(sys.argv) > 1 else ""
|
||
|
|
|
||
|
|
print_header("AOM Multimodal Live Test")
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||
|
|
tmpdir = Path(tmpdir)
|
||
|
|
|
||
|
|
# --- Create test files ---
|
||
|
|
print_header("1. Creating test files")
|
||
|
|
|
||
|
|
# Text file
|
||
|
|
text_file = tmpdir / "seo_report.txt"
|
||
|
|
text_file.write_text(
|
||
|
|
"Q1 2026 SEO Report\n"
|
||
|
|
"Organic traffic: 45,000 sessions (+12% QoQ)\n"
|
||
|
|
"Top keywords: running shoes (pos #3), trail shoes (pos #1)\n"
|
||
|
|
"New backlinks: 234 from 89 domains\n"
|
||
|
|
"Core Web Vitals: LCP 2.1s, FID 45ms, CLS 0.05\n"
|
||
|
|
)
|
||
|
|
print_ok(f"Created {text_file.name}")
|
||
|
|
|
||
|
|
# CSV file
|
||
|
|
csv_file = tmpdir / "campaigns.csv"
|
||
|
|
csv_file.write_text(
|
||
|
|
"campaign,impressions,clicks,ctr,cost,conversions\n"
|
||
|
|
"Spring Running,125000,4750,3.8%,$9262,127\n"
|
||
|
|
"Trail Shoes,89000,4539,5.1%,$6800,95\n"
|
||
|
|
"Newsletter Promo,45000,1890,4.2%,$2100,34\n"
|
||
|
|
)
|
||
|
|
print_ok(f"Created {csv_file.name}")
|
||
|
|
|
||
|
|
# Create a simple test PNG (1x1 pixel red dot)
|
||
|
|
import struct
|
||
|
|
import zlib
|
||
|
|
|
||
|
|
def make_test_png():
|
||
|
|
# Minimal valid PNG: 100x30 with "MaskanX Test" text-like pattern
|
||
|
|
width, height = 100, 30
|
||
|
|
raw = b""
|
||
|
|
for y in range(height):
|
||
|
|
raw += b"\x00" # filter none
|
||
|
|
for x in range(width):
|
||
|
|
if 10 <= x <= 90 and 5 <= y <= 25:
|
||
|
|
raw += b"\x20\x60\xff" # blue-ish
|
||
|
|
else:
|
||
|
|
raw += b"\xff\xff\xff" # white
|
||
|
|
compressed = zlib.compress(raw)
|
||
|
|
|
||
|
|
png = b"\x89PNG\r\n\x1a\n"
|
||
|
|
# IHDR
|
||
|
|
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
||
|
|
png += _png_chunk(b"IHDR", ihdr_data)
|
||
|
|
# IDAT
|
||
|
|
png += _png_chunk(b"IDAT", compressed)
|
||
|
|
# IEND
|
||
|
|
png += _png_chunk(b"IEND", b"")
|
||
|
|
return png
|
||
|
|
|
||
|
|
def _png_chunk(chunk_type, data):
|
||
|
|
chunk = chunk_type + data
|
||
|
|
return struct.pack(">I", len(data)) + chunk + struct.pack(">I", zlib.crc32(chunk) & 0xFFFFFFFF)
|
||
|
|
|
||
|
|
img_file = tmpdir / "dashboard_screenshot.png"
|
||
|
|
img_file.write_bytes(make_test_png())
|
||
|
|
print_ok(f"Created {img_file.name} (test PNG)")
|
||
|
|
|
||
|
|
# --- 2. Test BASIC mode (no multimodal key) ---
|
||
|
|
print_header("2. Basic Mode — text only, no multimodal key")
|
||
|
|
|
||
|
|
store = MemoryStore(tmpdir / "test_basic.db", dimensions=32)
|
||
|
|
await store.initialize()
|
||
|
|
embedder = FakeEmbeddingPipeline(dimensions=32)
|
||
|
|
config = AOMConfig(enabled=True, embedding_dimensions=32, importance_threshold=0.2)
|
||
|
|
|
||
|
|
ingest_basic = IngestAgent(store, embedder, real_llm_caller, config, multimodal=None)
|
||
|
|
|
||
|
|
# Text file should work
|
||
|
|
t0 = time.time()
|
||
|
|
mem = await ingest_basic.ingest_file(text_file)
|
||
|
|
print_ok(f"Text file ingested in {time.time()-t0:.1f}s | entities={mem.entities[:3]} | topics={mem.topics[:3]}")
|
||
|
|
|
||
|
|
# CSV should work
|
||
|
|
t0 = time.time()
|
||
|
|
mem = await ingest_basic.ingest_file(csv_file)
|
||
|
|
print_ok(f"CSV file ingested in {time.time()-t0:.1f}s | entities={mem.entities[:3]} | topics={mem.topics[:3]}")
|
||
|
|
|
||
|
|
# Image should FAIL in basic mode
|
||
|
|
try:
|
||
|
|
await ingest_basic.ingest_file(img_file)
|
||
|
|
print_fail("Image should have been rejected in basic mode!")
|
||
|
|
except RuntimeError as e:
|
||
|
|
print_ok(f"Image correctly rejected: {str(e)[:80]}")
|
||
|
|
|
||
|
|
stats = await store.get_stats()
|
||
|
|
print(f" Basic mode stats: {json.dumps(stats)}")
|
||
|
|
await store.close()
|
||
|
|
|
||
|
|
# --- 3. Test ADVANCED mode (with Gemini key) ---
|
||
|
|
if not gemini_key:
|
||
|
|
print_header("3. Advanced Mode — SKIPPED (no Gemini key)")
|
||
|
|
print(" Pass Gemini API key as first argument to test multimodal:")
|
||
|
|
print(" python tests/live_multimodal_test.py YOUR_GEMINI_KEY")
|
||
|
|
else:
|
||
|
|
print_header("3. Advanced Mode — Gemini Flash Lite")
|
||
|
|
|
||
|
|
store2 = MemoryStore(tmpdir / "test_advanced.db", dimensions=32)
|
||
|
|
await store2.initialize()
|
||
|
|
|
||
|
|
mm = MultimodalProcessor(
|
||
|
|
provider="gemini",
|
||
|
|
api_key=gemini_key,
|
||
|
|
model="gemini-2.5-flash-lite",
|
||
|
|
)
|
||
|
|
|
||
|
|
ingest_adv = IngestAgent(store2, embedder, real_llm_caller, config, multimodal=mm)
|
||
|
|
|
||
|
|
# Text file still works
|
||
|
|
t0 = time.time()
|
||
|
|
mem = await ingest_adv.ingest_file(text_file)
|
||
|
|
print_ok(f"Text file: {time.time()-t0:.1f}s | {mem.topics[:3]}")
|
||
|
|
|
||
|
|
# Image now works via Gemini
|
||
|
|
t0 = time.time()
|
||
|
|
try:
|
||
|
|
mem = await ingest_adv.ingest_file(img_file)
|
||
|
|
print_ok(f"Image file: {time.time()-t0:.1f}s | content={mem.content[:150]}...")
|
||
|
|
print(f" Entities: {mem.entities}")
|
||
|
|
print(f" Topics: {mem.topics}")
|
||
|
|
print(f" Metadata: {mem.metadata}")
|
||
|
|
except Exception as e:
|
||
|
|
print_fail(f"Image processing failed: {e}")
|
||
|
|
|
||
|
|
# Query across both text and image memories
|
||
|
|
print_header("4. Query across multimodal memories")
|
||
|
|
query_agent = QueryAgent(store2, embedder, real_llm_caller, config)
|
||
|
|
t0 = time.time()
|
||
|
|
result = await query_agent.query("What do we know about our marketing performance?")
|
||
|
|
print(f" Answer ({time.time()-t0:.1f}s): {result.answer[:300]}")
|
||
|
|
print(f" Citations: {len(result.citations)}")
|
||
|
|
|
||
|
|
stats2 = await store2.get_stats()
|
||
|
|
print(f"\n Advanced mode stats: {json.dumps(stats2)}")
|
||
|
|
|
||
|
|
await mm.close()
|
||
|
|
await store2.close()
|
||
|
|
|
||
|
|
print_header("DONE!")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
asyncio.run(main())
|