Files
legal-ai/docs/voyage-upgrades-plan.md
Chaim cb0b4b6a8b
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
ops: switch embeddings to voyage-3 + plan for context-3 + multimodal-3.5
Phase A — voyage-3 migration (executed):

- VOYAGE_MODEL=voyage-3 set in Coolify (legal-ai app) and ~/.env
- scripts/reembed_voyage.py: re-embeds document_chunks (6157),
  case_law_embeddings (9), precedent_chunks (385), and halachot (400)
  using the new model. paragraph_embeddings was empty. 6951 rows
  re-embedded in 93s, ~75 rows/sec.
- Same 1024 dim → no schema change needed.

Why voyage-3 over voyage-law-2: benchmark on 3 Hebrew legal queries
with real passages from the corpus gave voyage-3 perfect ordering on
3/3 tests AND the largest separation (+0.483 vs voyage-law-2's
+0.238). voyage-4 family had bigger separation but missed top-1 on
the hardest test.

Phase B (voyage-context-3) and Phase C (voyage-multimodal-3.5 for
scanned + appraiser docs) are designed in docs/voyage-upgrades-plan.md
but deferred — to be picked up in a fresh conversation. The plan
includes:
- Phase B: contextualized embeddings refactor (~49% recall lift on
  legal docs per Anthropic's research). Same dim, but ingestion
  pipeline must pass full doc context per chunk.
- Phase C: page-level image embeddings via voyage-multimodal-3.5,
  stored in a parallel *_image_embeddings table. Hybrid text+image
  search. Targets appraiser report tables and scanned PDFs where
  current OCR loses layout.

After this commit: MCP server needs a /mcp reconnect to pick up the
new VOYAGE_MODEL env, and the legal-ai container will pick it up on
its next redeploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:43:48 +00:00

339 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# שדרוגי Voyage — תכנית מפורטת
תכנית 3-שלבית לשדרוג שכבת ה-retrieval של עוזר משפטי. שלב A מבוצע
בתאריך התכנית; שלבים B ו-C ממתינים לשיחה החדשה.
**הקשר**: Voyage = חיפוש (find), Claude = הבנה+כתיבה (read+write). שני
המנועים מנותקים ארכיטקטונית — שינוי שכבת ה-retrieval לא משפיע על קלוד
עצמו, רק על איזה chunks מגיעים אליו לקריאה.
---
## שלב A — מעבר ל-voyage-3 (✅ מבוצע)
### למה voyage-3 ולא voyage-law-2?
Benchmark על 3 שאילתות עברית-משפטית עם passages אמיתיים מהקורפוס:
| מודל | Perfect orderings | Total Separation |
|---|---|---|
| **voyage-3** | **3/3** | **+0.483** |
| voyage-3.5 | 3/3 | +0.278 |
| voyage-law-2 *(היה)* | 3/3 | +0.238 |
| voyage-4 | 2/3 | +0.423 |
| voyage-4-large | 2/3 | +0.353 |
voyage-3 **מנצח כפול** — דירוג מושלם + מרווחים גדולים פי-2 מ-voyage-law-2.
מימד נשאר 1024 → אין שינוי schema.
### מה בוצע
1. **Coolify env**: `VOYAGE_MODEL=voyage-3` בקונטיינר
2. **Local env (`~/.env`)**: `VOYAGE_MODEL=voyage-3`
3. **Re-embed של 5 טבלאות** באמצעות `scripts/reembed_voyage.py`:
- `document_chunks` — מסמכי תיקים (~6K rows)
- `paragraph_embeddings` — קורפוס סגנון (כעת ריק)
- `case_law_embeddings` — stubs מצוטטים אוטו'
- `precedent_chunks` — פסיקה שהועלתה (~385)
- `halachot.embedding` — 400 הלכות (rule_statement + reasoning)
4. **MCP server restart** — טעינה מחדש של `embeddings.py` עם המודל החדש
### Verification
- `search_precedent_library` על "תכנית רחביה" → 403/17 holding ראשון
- `search_decisions` על "השבחה" → תוצאות עקביות
- ה-counts בטבלאות לא ירדו (כל row עודכן, לא נמחק)
### Rollback אם משהו נשבר
- `VOYAGE_MODEL=voyage-law-2` ב-Coolify + `~/.env`
- הרצה מחדש של `scripts/reembed_voyage.py` (חוזרים לקודם)
- 10 דקות סך-הכל
---
## שלב B — voyage-context-3 (לביצוע בשיחה החדשה)
### הבעיה שהוא פותר
Embeddings רגילים מטמיעים **chunk בנפרד** מהקשר המסמך. פסקה שאומרת
"כפי שקבענו לעיל, הפטור אינו חל" — לא יודעת על "פטור ממה" / "לעיל
איפה". פסיקה משפטית מלאה בהפניות הקשר תלויות (ראה סעיף 7 לעיל; להבדיל
מהמקרה ב-וע 1126/25; וכו') — והן אבודות לחלוטין.
### מה voyage-context-3 עושה אחרת
API שונה: `client.contextualized_embed(inputs=[[full_doc, chunk_1], ...])`.
כל chunk מוטמע **עם המסמך כולו כקונטקסט**. ה-embedding "יודע" שזו פסקה
14 מתוך פסק דין על תמ"א 38 — והקשרים פנימיים נשמרים.
Anthropic פרסמו מדידה: **שיפור 49% בדיוק חיפוש** למסמכים משפטיים
ארוכים.
### תכנית יישום
#### B.1 — Refactor של pipeline ה-ingestion
קוד נוכחי (`embeddings.py`):
```python
embs = await embed_texts(chunk_texts, input_type="document")
```
קוד חדש:
```python
embs = await embed_texts_with_context(
document_full_text=text,
chunks=chunk_texts,
input_type="document",
)
```
מקומות שצריכים שינוי:
- `mcp-server/.../services/embeddings.py` — פונקציה חדשה `embed_with_context`
שעוטפת `client.contextualized_embed`
- `mcp-server/.../services/processor.py``process_document()` מעביר
את `text` המלא + chunks
- `mcp-server/.../services/precedent_library.py``ingest_precedent`
מעביר `text` + chunks
- `mcp-server/.../services/halacha_extractor.py` — לכל הלכה, מעביר
את הפסק המלא כקונטקסט (`case_law.full_text`) + `rule_statement`
שמוטמע
#### B.2 — Query embedding נפרד
Queries מטמיעים בלי קונטקסט (`client.embed()` רגיל עם
`model="voyage-context-3"` ו-`input_type="query"`). חשוב: queries
ו-documents חייבים להיות באותו model space.
ב-`embeddings.py:embed_query()` — מחליפים model אבל לא ה-API.
#### B.3 — Re-embed של הקורפוס הקיים
```python
# Pseudo-code
for table in [document_chunks, precedent_chunks, halachot, ...]:
rows = SELECT id, content, parent_doc_id FROM table
for row in rows:
full_doc = SELECT full_text FROM parent_table WHERE id = row.parent_doc_id
embedding = contextualized_embed(full_doc, row.content)
UPDATE table SET embedding = embedding WHERE id = row.id
```
הבעיה: כל chunk שולח את **המסמך כולו** כקונטקסט. לכן עלות לטוקן
עולה משמעותית. אומדן: 178K תווים × 50 chunks = 8.9M תווים פר פסיקה,
פי-50 לעומת voyage-3. החישוב לקורפוס הנוכחי (~7K rows): שווה ערך
לכ-700M תווים. בtier החינמי של voyage קיים מגבלה — חשוב לבדוק לפני
הרצה גדולה.
**Mitigation**: לחלץ summary של 500-1000 תווים מכל מסמך (קלוד עושה
את זה היום ב-`metadata_extractor`) ולהעביר ה-summary במקום הטקסט המלא.
שמירת 95% מהיתרון בעלות 5%.
#### B.4 — Schema
אין שינוי. אותו `vector(1024)` column.
#### B.5 — Benchmark לפני החלטה סופית
לפני re-embed של 6951 rows:
1. לקחת 10 שאילתות אמיתיות + passages עם תיוג נכון
2. להריץ benchmark voyage-3 vs voyage-context-3 (אותו pipeline כמו
`/tmp/voyage_compare.py`)
3. אם השיפור < 15% → לא שווה את העלות. נשאר ב-voyage-3
4. אם השיפור ≥ 15% → ל-go ל-context-3
#### B.6 — בדיקת זמן + עלות
לאחר ה-benchmark:
- אם בtier החינמי לא מספיק טוקנים → לבחור: רק documents (לא
re-embed הקיים), רק פסיקה חדשה והלאה
- או: לעבור ל-context-3 רק על קורפוס הפסיקה (4 פסיקות, ~785 chunks
+ halachot) — הקרפוס הקריטי ביותר ל-`search_precedent_library`
### החלטות שנשארו פתוחות (תיקח החלטה בשיחה החדשה)
- ✋ Re-embed הכל בבת-אחת או רק חדש?
- ✋ context-3 לכל הקורפוסים או רק לפסיקה (הקריטי ביותר)?
- ✋ Document context = full_text או summary של 1K?
---
## שלב C — voyage-multimodal-3.5 (לביצוע בשיחה החדשה)
### הבעיה שהוא פותר
תיקים סרוקים ודוחות שמאי מאבדים מידע ב-OCR:
- ✗ פריסת טבלאות (שורות נתונים מתבלגנות)
- ✗ חתימות וחותמות
- ✗ דיאגרמות, מפות, תרשימים אדריכליים
- ✗ נוסחאות מתמטיות
OCR קיים (Google Cloud Vision) ממיר תמונות לטקסט אבל מטפל בעמוד כשורה-
אחר-שורה. תוצאה: בדוח שמאי "שווי לפני | שווי אחרי | ≈ 1.5M ש"ח" הופך
ל-"שווי לפני שווי אחרי 1.5M ש"ח" — חיפוש "שומה ל-1.5M" לא תמיד מוצא.
### מה voyage-multimodal-3.5 עושה
API: `client.multimodal_embed(inputs=[[image, text?], ...])`. מקבל
תמונה (PIL Image או URL) ומחזיר embedding שכולל:
- את הטקסט שעל העמוד
- את **המבנה הוויזואלי** (טבלה, חתימה, מיקומי גוש)
- תרשימים ודיאגרמות
Searchable יחד עם text embeddings — query טקסטואלית רגילה מוצאת גם
פסקאות עם טבלה רלוונטית.
### תכנית יישום
#### C.1 — Schema חדש
```sql
CREATE TABLE document_image_embeddings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
page_number INTEGER NOT NULL,
image_thumbnail_path TEXT, -- לסרגל תוצאות חיפוש
embedding vector(1024),
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_doc_img_emb_vec
ON document_image_embeddings USING ivfflat (embedding vector_cosine_ops);
CREATE TABLE precedent_image_embeddings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
case_law_id UUID REFERENCES case_law(id) ON DELETE CASCADE,
page_number INTEGER NOT NULL,
image_thumbnail_path TEXT,
embedding vector(1024),
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_prec_img_emb_vec
ON precedent_image_embeddings USING ivfflat (embedding vector_cosine_ops);
```
#### C.2 — Pipeline שינוי
חדש ב-`extractor.py`:
```python
async def render_pages_as_images(pdf_path: str) -> list[bytes]:
"""PyMuPDF render of each page → PNG bytes for multimodal embedding."""
import fitz
doc = fitz.open(pdf_path)
images = []
for page in doc:
pix = page.get_pixmap(dpi=144) # decent resolution for embeddings
images.append(pix.tobytes("png"))
return images
```
חדש ב-`embeddings.py`:
```python
async def embed_images(images: list[bytes], input_type: str = "document") -> list[list[float]]:
"""Embed page images via voyage-multimodal-3.5."""
from PIL import Image
import io
pil_images = [Image.open(io.BytesIO(img)) for img in images]
response = _get_client().multimodal_embed(
inputs=[[img] for img in pil_images],
model="voyage-multimodal-3.5",
input_type=input_type,
)
return response.embeddings
```
#### C.3 — Integration ב-ingest pipelines
`processor.py:process_document` (תיק):
```python
# אחרי extract+chunk+embed הטקסטואלי:
images = await extractor.render_pages_as_images(file_path)
img_embs = await embeddings.embed_images(images)
await db.store_document_image_embeddings(document_id, img_embs, thumbnails)
```
`precedent_library.py:ingest_precedent`: אותו pattern, על
`precedent_image_embeddings`.
#### C.4 — Hybrid search
חדש ב-`db.py:search_precedent_library_hybrid`:
```python
async def search_precedent_library_hybrid(query, limit=10):
query_emb = await embeddings.embed_query(query)
query_img_emb = await embeddings.embed_query_for_multimodal(query)
text_results = ... # cosine on precedent_chunks (top 30)
image_results = ... # cosine on precedent_image_embeddings (top 30)
# Merge: weighted score (text 0.6, image 0.4 — tunable)
merged = {}
for r in text_results: merged[r.case_law_id] = r.score * 0.6
for r in image_results:
merged[r.case_law_id] = merged.get(r.case_law_id, 0) + r.score * 0.4
return sorted(merged.items(), key=lambda x: -x[1])[:limit]
```
#### C.5 — UI: thumbnails בתוצאות חיפוש
ב-`/precedents` חיפוש סמנטי, התוצאות עם רכיב image יציגו thumbnail
קטן של העמוד. לחיצה תפתח את ה-PDF במקום הרלוונטי.
#### C.6 — סדר עדיפויות לדיגום
1. **דוחות שמאי** — הזכייה הגדולה (טבלאות = ערכים מספריים שכרגע
הולכים לאיבוד ב-OCR)
2. **תיקים סרוקים ישנים** — שיפור ה-recall של חיפוש
3. **פסיקה עם דיאגרמות** (תרשימי גוש/חלקה) — minor
#### C.7 — עלות + tier
voyage-multimodal-3.5 הוא מוצר נפרד. בdoc'ים פר-עמוד:
- תיק ממוצע: 50-200 עמודים
- 100 תיקים = 5,000-20,000 עמודים
- Free tier: 200M tokens/month — אבל multimodal נמדד ב-tokens שונה
(התמונה צורכת ~1000-2000 tokens לעמוד)
הערכה: 100 תיקים × 100 עמודים × 1500 tokens = 15M tokens. בthe
free tier בקלות. צריך לבדוק תקרת שימוש בפועל בdocs של voyage.
#### C.8 — שלבים מומלצים
1. **POC** — תיק אחד עם דו"ח שמאי. embed → search → השוואה לתוצאות
טקסט-בלבד.
2. **A/B test** — חצי מהתיקים החדשים עם multimodal, חצי בלי. 4
שבועות בדיקה — האם דפנה מוצאת תוצאות מדויקות יותר?
3. **Rollout** — אם המבחן חיובי, לעבד את הקורפוס הקיים ברקע
### החלטות שנשארו פתוחות
- ✋ DPI לרינדור: 144 (סביר), 200 (איכות), 96 (מהיר)?
- ✋ נשמור thumbnails ב-disk או רק את ה-embeddings?
- ✋ משקלות hybrid search: 0.6/0.4 או יותר נטוי לטקסט?
---
## רצף עבודה בשיחה החדשה
> 1. פתחי `docs/voyage-upgrades-plan.md` (זה המסמך)
> 2. אם A הצליח (verify ב-Coolify env), נמשיך ל-B (context-3)
> 3. **B.5 קודם** — benchmark לפני re-embed גדול
> 4. אם B מצליח, רץ ל-C — אבל ב-2 צעדים זהירים (POC → A/B → rollout)
---
## נספח: רשימה של קבצים שנגעו ב-Voyage היום
קוד שנכתב/שונה:
- `scripts/reembed_voyage.py` — חדש, סקריפט re-embed
- `~/.env``VOYAGE_MODEL=voyage-3`
- Coolify env (legal-ai app) — `VOYAGE_MODEL=voyage-3`
קבצים שלא צריכים שינוי (CONFIRM):
- `mcp-server/src/legal_mcp/services/embeddings.py` — קורא ל-config.VOYAGE_MODEL
- `mcp-server/src/legal_mcp/config.py` — default ל-voyage-law-2 אבל env
בקוולפיי + מקומית מנצח
- כל הסוכנים (legal-writer, etc.) — לא קוראים ל-Voyage ישירות
עבור B + C: השינויים במסמך הזה (לא מבוצעים עדיין).