536358093f
The intake bot now accepts a photo of a business card in the intake room and turns it into the same new-investor proposal a typed note would. The only new step is image -> text; everything downstream (parse, fuzzy match, in-thread approval, log-communication write) is reused unchanged. M3 was deferred only because Spark Control had no vision model. That blocker is gone: the daily-driver Qwen is vision-capable under the same model id, and the gateway forwards OpenAI multimodal content untouched, so no gateway/server/s9pk change is needed -- this ships bot-only (git pull + rebuild on the Spark). Transcribe-then-reuse (not vision-straight-to-JSON) is deliberate: the transcription becomes the source text the email-integrity rule checks against, so a mis-read address can't reach the CRM unapproved -- same guarantee as the text path. Card commits tag source="matrix_card" for the audit log. - llm.chat_vision: multimodal /v1/chat/completions, same model, same gateway - spark.transcribe_card: faithful card->text, "" on a non-card (NONE sentinel) - bot.on_image/handle_card: download image, transcribe, hand to handle_intake - crm_client: source provenance overridable via the proposal's _source key - tests: test_spark.py + a provenance case; 41/41 suite green
46 lines
1.8 KiB
Python
46 lines
1.8 KiB
Python
"""Tests for the business-card vision wrapper (pure logic, no network — chat_fn is stubbed)."""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
import spark # noqa: E402
|
|
|
|
|
|
def test_transcribe_card_returns_faithful_text():
|
|
captured = {}
|
|
|
|
def fake_chat(prompt, image_b64, mime="image/jpeg", system=None, max_tokens=600):
|
|
captured["image_b64"] = image_b64
|
|
captured["mime"] = mime
|
|
captured["system"] = system
|
|
return "Jane Doe\nGeneral Partner\nAcme Capital LLC\njane@acme.com\n+1 555 123 4567"
|
|
|
|
out = spark.transcribe_card("Zm9vYmFy", mime="image/png", chat_fn=fake_chat)
|
|
# The transcription is passed through verbatim — email survives for the integrity check.
|
|
assert "jane@acme.com" in out
|
|
assert "Acme Capital LLC" in out
|
|
# The image + mime reached the vision call; the card system prompt was used.
|
|
assert captured["image_b64"] == "Zm9vYmFy"
|
|
assert captured["mime"] == "image/png"
|
|
assert "business card" in (captured["system"] or "").lower()
|
|
|
|
|
|
def test_transcribe_card_none_sentinel_becomes_empty():
|
|
# The model replies NONE for an unreadable / non-card image → we return "" so the bot can
|
|
# ask for a clearer photo instead of feeding garbage into the intake parser.
|
|
assert spark.transcribe_card("x", chat_fn=lambda *a, **k: "NONE") == ""
|
|
assert spark.transcribe_card("x", chat_fn=lambda *a, **k: " none ") == ""
|
|
|
|
|
|
def test_transcribe_card_strips_whitespace():
|
|
assert spark.transcribe_card("x", chat_fn=lambda *a, **k: " Acme\n ") == "Acme"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
|
for fn in fns:
|
|
fn()
|
|
print(f"ok {fn.__name__}")
|
|
print(f"\n{len(fns)} passed")
|