1מבוא
מה הסוכן עושה, ולמי הוא מיועד.
כלי שיחתי לאנליסט השקעות בתשתיות תעופה, על פני כל היקום של 515 שדות התעופה ב-FAA Commercial Service Enplanements — לא סט מצומצם לדוגמה. הוא עונה על ארבע צורות שאלה שונות מהותית (לא ארבע ניסוחים לאותה שאלה): דירוג מסונן, השוואת זוגות, אגרגציה על ישות בודדת, ומדד מחושב עם הסבר סיבתי — על ידי קריאה לפונקציות Python דטרמיניסטיות והסבר התוצאה שלהן. הוא לעולם לא מחשב ציון, אחוז, או דירוג בעצמו.
הממצא המרכזי: תחת חמשת הקריטריונים המשוקללים לכיוון "פוטנציאל צמיחה" (ולא "גודל נוכחי"), השדה הכי גדול לא מנצח אוטומטית — LAX מדורג 67 מתוך 144 שדות זכאים, והשניים המובילים בפועל (Nashville ו-Denver) מרוחקים 0.30% בלבד, כשכל אחד מחמשת המשקלים יכול להפוך את הסדר בשינוי של 5%.
ארבע צורות השאלה
| צורה | דוגמה מהבריף | פרימיטיב | למה לא ניתן למחזר את מסלול הדירוג |
|---|---|---|---|
| דירוג מסונן | מועמדים להרחבה בניו-אינגלנד | find_items → compare_items | — |
| השוואה + ישות עמומה | LA מול Santa Ana | resolve_entity → compare_items | "LA" עמום אמיתי — הרזולבר חייב לדעת להגיד את זה |
| אגרגציה על ישות בודדת | % טיסות ארוכות-טווח מ-Anchorage | aggregate_records | חלק יחסי בתוך שדה אחד, לא השוואה בין שדות |
| מדד נגזר + סיבתיות | ביקוש בלתי-ממומש ב-SFO, ולמה | estimate_derived_metric | הכמות לא קיימת באף מקור נתונים — חייבת מודל, עם הנחות-יסוד מוצמדות |
2מספרים לזכור
גיליון-הצצה מהיר לפני ההגנה — כל מספר כאן צריך לצאת בעל-פה, בלי היסוס.
ce18c49):
- רצפת הנרמול של
capacity_pressureהייתה תקועה על ערך ישן (201,438.53 במקום 287,264.425 — האחוזון ה-5 האמיתי של ה-144 הזכאים). זה שינה את כל המספרים למעלה: LAX 69→67, הפער 0.41%→0.30%, הקריטריון הרגיש ביותרcatchment_monopoly→traffic_growth, וטווח הפיכת-המנצח 5–10%→5% קבוע. נמצא ע"יtest_real_dataset_scores_are_pinned_to_known_values. ההחלטה: לתקן את הקבוע, לא לדווח כפער — כי זו טעות, לא trade-off. - ערכי ה-r בטבלת הקריטריונים ב-
DESIGN_DOC.mdנמדדו במקור על כל 515 השדות, לא על ה-144 המדורגים — והדוח לא ציין זאת. על האוכלוסייה המדורגת המספרים שונים מהותית (traffic_growth−0.02→−0.29,catchment_monopoly−0.05→−0.29,regional_demand_growth+0.18→+0.02,capacity_pressure0.89→0.85). שתי האוכלוסיות מוצגות כעת. אם נשאלים: טענת אי-הקורלציה שורדת בכל אחת מהן, אבל האוכלוסייה המדורגת היא זו שרלוונטית — והיא זו שמוצגת כעת.
3עקרונות יסוד
כללי ברזל שחוזרים בכל שכבה בקוד, ושכדאי לדעת לצטט בעל-פה.
אפס I/O, אפס LLM בליבה הדטרמיניסטית
scoring.py, entity_resolution.py, analytics.py ו-runway_geometry.py
לא עושים אף קריאת רשת, קריאת קובץ, או קריאת משתנה סביבה — נאכף גם ע"י מבחן mutation,
לא רק במוסכמה.
Hand-rolled agent loop, בלי framework
אין LangChain, אין LangGraph. הלולאה
(agent_loop.py) היא כ-70 שורות while פשוט על ספק אחיד — שליטה
מלאה במסלול, בטרמינציה, ובעקבות ניפוי השגיאות.
שגיאת כלי היא נתון, לא קריסה
כל חריגה שנזרקת בתוך כלי נתפסת ומומרת ל-{"error": ...} שחוזר למודל כטקסט. תפיסה
זו נבדקת גם דרך agent_loop.py וגם דרך /chat שתופס
MaxTurnsExceeded ומחזיר תשובה חלקית במקום 500.
לעולם לא להמציא מספר
NEVER_COMPUTE_RULE ב-system_prompt.py אוסר על המודל לחשב, ואוכף
בפועל: כל כלי מחזיר פירוק לרכיבים (ערך גולמי, ציון מנורמל, משקל, תרומה) — לעולם לא מספר בודד.
מזהה שגוי נכשל בקול רם, לא בשקט
NEVER_INVENT_IDS_RULE מנחה את המודל לקרוא ל-resolve_entity — אבל זה
לא אטום: תמלולי eval אמיתיים מראים ש-gpt-4o-mini לפעמים
מספק קוד ידוע (LAX) ישירות. הערבות בפועל: מזהה שגוי גורם ל-compare_items
לזרוק UnknownItemError — נכשל, לא מדרג בטעות.
פלט של כלי הוא תמיד "נתון לא מהימן"
כל תוצאה שמגיעה מכלי נעטפת (wrap_untrusted) בתגית <untrusted_data>
לפני שהיא חוזרת לשיחה, וסרוקה (scan_for_injection) לתבניות prompt-injection
נפוצות — פילטר regex דטרמיניסטי, לא קריאת LLM שנייה.
4ארכיטקטורה כללית — שכבות
מבט-על מלמעלה למטה: מהממשק למשתמש ועד לתשתית.
static/index.html · static/markdown.js · static/voice.js · app/cli.py
app/main.py (FastAPI + SSE) · app/voice_api.py
app/agent_loop.py · app/conversation.py
app/tools.py — 11 כלים, כל אחד עם פירוק לרכיבים
scoring.py · entity_resolution.py · analytics.py · runway_geometry.py
dataset.py · guardrails.py · system_prompt.py
providers/llm/ · providers/stt/ · providers/tts/
config.py · .env · data/
זהו כבר לא ה-scaffold הגנרי — כל שכבה כאן מלאה בדומיין אמיתי: 515 שדות תעופה, חמישה קריטריונים מוגנים, ושתי שכבות קול שתיהן פועלות בפועל.
5זרימת בקשה טיפוסית
מה קורה בפועל, צעד אחר צעד — גם לשאלה כתובה וגם לשאלה מדוברת.
POST /chat/stream (SSE, מה שה-UI
באמת משתמש בו) או POST /chat (תשובת JSON יחידה) — שניהם ב-app/main.pyrun_agent() ב-agent_loop.pydone — סוף הלולאהTOOL_REGISTRY ל-app/tools.py, ונדחפת
ל-UI כאירוע tool_call ברגע שהיא מסתיימתdataset.py מספק את הנתונים; scoring.py /
analytics.py / runway_geometry.py מחשבים — תמיד פירוק לרכיביםguardrails.wrap_untrusted() + scan_for_injection() לפני
שהתוצאה חוזרת לשיחהmax_turnsMaxTurnsExceeded נושא את התמלול החלקי ואת יומן הכליםומה קורה כששאלה נאמרת בקול
static/voice.js — דגימה ל-16kHz, VAD מבוסס אנרגיהPOST /voice/transcribegpt-4o-mini-transcribe) → טקסטPOST /chat/stream בדיוק/voice/chat נפרד — מכאן ואילך זו בדיוק אותה זרימה כמו טקסטPOST /voice/speakgpt-4o-mini-tts / Google Neural2) ומתנגנתPOST /voice/interruptconversation.py)
למה שבאמת נשמע6קריטריוני הדירוג — הרכיב הכי-מוגן בכל הפרויקט
חמישה קריטריונים, משקלים 25/25/20/15/15, על סט זכאות של 144 מתוך 515 שדות.
הבעיה שהמסגור הזה פותר
הבריף שואל אילו שדות "renovations will be most profitable based on increased flight and passenger capacity" — זו שאלה על עתודה בלתי-ממומשת, לא על גודל נוכחי. ציון מורכב על תעבורה נוכחית ידרג את השדות הכי גדולים ראשונים ולא יגיד כלום על האם הרחבה שלהם באמת משתלמת. הקריטריונים למטה נבחרו כדי לשקף עתודה, לא גודל.
| קריטריון | משקל | מקור | למה זה מייצג עתודה |
|---|---|---|---|
traffic_growth | 25 | FAA, שינוי % CY2024→CY2025 | לחץ שכבר עולה — r=−0.02 (515) / −0.29 (144) עם גודל |
regional_demand_growth | 25 | אוכלוסיית מחוז, CAGR 2022→2025 | סימן הביקוש היחיד באזור — r=+0.18 (515) / +0.02 (144) |
catchment_monopoly | 20 | מרחק מתחרה משדה-שירות קרוב ביותר | האם ביקוש יכול "לברוח" — r=−0.05 (515) / −0.29 (144) |
capacity_pressure | 15 | נוסעים ÷ מספר מסלולי-נשא | פרוקסי הצפיפות היחיד הזמין; r=0.89 (515) / 0.85 (144) עם absolute_scale — מוצהר, לא מוסתר |
absolute_scale | 15 | נוסעים, CY2025 ראשוני | "גודל הפרס" — נשאר תחת תקרת ה-25% שהעיצוב קבע לעצמו |
traffic_growth +
regional_demand_growth) נושאים 50% מהמשקל; שני הקריטריונים "מגמת-גודל"
(capacity_pressure + absolute_scale, מתואמים ביניהם) נושאים 30%.
בדיקת שפיות: LAX מדורג 67 מתוך 144, לא ראשון.
נרמול ונתונים חסרים
כל קריטריון מנורמל min-max ל-[0,1] מול חיתוך אחוזון 5/95 של
הסט הזכאי (144), חוקי אחד עקבי לכל הקריטריונים — לא log-transform נפרד לכל
קריטריון עקום. נתוני תעופה ציבוריים "מרוטשים" (ragged) — 14 מתוך 515 שדות
(פוארטו ריקו ואיים) חסרי נתון אוכלוסייה. scoring.py מוריד את הרכיב החסר ומנרמל מחדש
את שאר המשקלים; מתחת לסף כיסוי (0.5, לא כולל) השדה נפסל מהדירוג לגמרי ומדווח
בנפרד — ציון שנבנה על שאריות גרוע מהשמטה כנה.
רגישות משקלים — כמה המשקלים באמת משנים
פלט אמיתי של weight_robustness_report על 144 השדות, 19.8.2026 (לאחר תיקון רצפת הנרמול):
baseline_top: BNA (Nashville, 0.6333) — runner-up: DEN (Denver, 0.6314)
most_sensitive_criterion: traffic_growth (flips winner at 0.95x)
criterion current_weight flip_factor
traffic_growth 25 0.95
regional_demand_growth 25 0.95
catchment_monopoly 20 0.95
capacity_pressure 15 1.05
absolute_scale 15 1.05
כל חמשת הקריטריונים בתיקו, לא רק זה שמופיע בשם. כל flip_factor מרוחק
בדיוק 5% מ-1.0. most_sensitive_criterion בוחר תווית אחת מתוך תיקו חמישייתי
(min() על |factor − 1.0|, ההתאמה הראשונה מנצחת) — זה כבר השתנה פעם אחת
(catchment_monopoly→traffic_growth) בעקבות תיקון נתונים שלא נגע במשקל של
אף קריטריון. הממצא המרכזי הוא לא באג להסביר — מקום #1 לא מכריע. זו התשובה הכנה
ל"למה המשקלים האלה": הצורה הכללית של הדירוג היא מה שהמשקלים מגנים עליו, לא #1 ספציפי.
| קריטריון | τ ב-×0.5 | שינוי מנצח ב-×0.5 | τ ב-×2.0 | שינוי מנצח ב-×2.0 |
|---|---|---|---|---|
traffic_growth | 0.841 | BNA→DEN | 0.764 | BNA→PVU |
regional_demand_growth | 0.858 | BNA→DEN | 0.837 | BNA→XNA |
catchment_monopoly | 0.837 | BNA→DEN | 0.769 | לא |
capacity_pressure | 0.907 | לא | 0.851 | BNA→DEN |
absolute_scale | 0.915 | לא | 0.875 | BNA→DEN |
Kendall tau נשאר 0.76–0.92 גם כשמכפילים או מחצים משקל בודד —
הסדר הכללי הרבה יותר יציב מ-#1. BNA ו-DEN בתיקו מסיבות הפוכות
(BNA מנצח על צמיחה+מונופול, DEN על גודל+צפיפות) —
compare_items מדווח tied_at_top/decisive מפורשות כשהשניים
הראשונים בטווח 0.005, וכלל 6 ב-system_prompt.py מחייב את המודל להציג תיקו כתיקו.
שער הזכאות — למה 144 ולא 515
דירוג על כל 515 השדות הכניס שדות עם אלפי נוסעים בודדים לראש 50 — פרוקסי-רעש של צמיחה-באחוזים על
בסיס אפסי (שדה אחד ב-+126,403% שנתי על 37,951 נוסעים). הזכאות מוגבלת ל-FAA hub
class L/M/S — פילטר דירוג, לא חיתוך נתונים: השדות עדיין ניתנים לשליפה בשם
דרך find_items/resolve_entity. נבחרה סיווג הרגולטור עצמו על פני רצפת
נוסעים שהומצאה — "השתמשתי בהגדרה של הרגולטור למה זה שדה ראשי", לא במספר עגול שנבחר כדי שהתוצאה
תיראה נכון.
compare_items בלבד —
שלושת כלי הדירוג האחים (rank_by_priorities, analyze_weight_sensitivity,
weight_robustness_report) שיחזרו בדיוק את כשל New Bedford Regional
שהשער נועד למנוע. שער שהמודל צריך לזכור לעבור דרכו הוא לא כלל, הוא הצעה — עכשיו הוא בכל ארבעת
הכלים.
7צנרת הנתונים
חמישה מקורות ציבוריים, ללא מפתח, נבנים פעם אחת ל-data/ — לא ברגע הבקשה.
| מקור | מספק |
|---|---|
| OurAirports | זהות, גיאוגרפיה, גיאומטריית מסלולים |
| FAA Commercial Service Enplanements | נפח נוסעים + סיווג hub class |
| US Census PEP — אוכלוסיית מחוז | הסימן היחיד בצד-הביקוש |
| BTS T-100 Segment Summary, מסונן ל-ANC | פרוקסי טיסות-ארוכות לשאלה 3 |
| FAA NAS Status (חי) | סטטוס תפעולי — מחוץ למסלול הדירוג במכוון |
app/dataset.pydata/processed_data/
בזמן ריצה — כל שאר המודולים נשארים טהורים וניתנים לבדיקה בלי fixturesdata/refresh_data.py8רכיבי הליבה
קובץ אחר קובץ — מה כל אחד עושה ואיזה עיקרון הוא אוכף.
app/agent_loop.py
לולאת ה-Agentהלולאה עצמה — while פשוט, כ-70 שורות, בלי framework.
tool_calls,
מריץ אותם דרך TOOL_REGISTRY, עוטף תוצאה כ"נתון לא מהימן", מוסיף להיסטוריה, לולאה שובmax_turns (ברירת מחדל 6) → MaxTurnsExceeded
עם תמלול חלקי ויומן כלים, לא crash חשוףapp/scoring.py
ליבה דטרמיניסטיתמנוע הדירוג המשוקלל — rank_items(), Criterion.normalize(),
חיתוך אחוזון, נרמול-מחדש של משקלים מעל coverage_threshold. הקובץ הכי "ניתן להגנה"
בכל הפרויקט.
sensitivity_analysis(), find_weight_flip_point()
— מאפשרים "מה קורה אם משקל משתנה" כמותית, במקום להשאיר את זה לתחושת בטןapp/entity_resolution.py
ליבה דטרמיניסטיתJaro-Winkler + Soundex (מומשו ידנית, ללא תלות חיצונית), פלוס שכבת metro ופלוס fallback מרחק-עריכה מוגבל לקודים קצרים.
METRO_AIRPORTSdecisive: boolapp/runway_geometry.py
ליבה דטרמיניסטית — חדשמחשב הפרדה בין מסלולים מקבילים ופגיעה בקיבולת נחיתה ממקורי מיקום מסלול ציבוריים,
לכל שדה. מזין את איבר "קיבולת מדוכאת-מזג-אוויר" ב-estimate_unmet_demand.
test_sfo_parallel_separation_matches_the_published_figure
— ההפרדה המחושבת ל-SFO (746.8 רגל) תואמת את הנתון הפומבי הידוע (~750 רגל)app/analytics.py
ליבה דטרמיניסטיתשלוש הצורות שהן לא דירוג.
find_items()aggregate_records()unknown_category +
known_categories + category_semantics מסבירים איזה פרוקסי אמיתי מתאיםestimate_derived_metric()/estimate_unmet_demand()test_declining_growth_is_clamped_not_credited_as_headroom)app/tools.py
שכבת חיבור — 1675 שורותהמקום היחיד שמחבר את הליבה הדטרמיניסטית למה שהמודל רואה.
11 הכלים החשופים למודל
| כלי | עונה על | קורא מ- |
|---|---|---|
find_items | "אילו שדות תואמים X" (סינון) | dataset.py |
compare_items | "השווה/דרג שדות" — כאן חי שער הזכאות | scoring.py |
rank_by_priorities | "אכפת לי מצמיחה יותר מצפיפות" (משקלים מותאמים) | scoring.py |
analyze_weight_sensitivity | "כמה משנה שינוי משקל X" | scoring.py |
weight_robustness_report | "כמה יציב הדירוג" (תיקו-כמעט, τ) | scoring.py |
list_criteria | "מה המשקלים שלך" (בלי צורך בפריטים) | קבועי scoring.py |
resolve_entity | "LA", "Ankorage" → מזהי פריט | entity_resolution.py |
get_item_metrics | נתונים גולמיים לשדה בודד, בלי דירוג | dataset.py |
aggregate_records | "% מ-X שהם Y" (חלק יחסי) | analytics.py |
estimate_derived_metric | "ביקוש בלתי-ממומש ב-SFO, ולמה" (מודל + גורמים) | analytics.py, runway_geometry.py |
get_live_airport_status | סטטוס תפעולי חי (סגירות/עיכובים) — מחוץ למסלול המדורג במכוון | nasstatus.faa.gov, זמן-אמת |
list_criteria() נוצר כי הסוכן פעם סירב לומר את משקלי הדירוג שלו כשנשאל
ישירות ("קנייני" — שום דבר בקוד לא אמר את זה; לא היה לו כלי לנתב אליו שאלת-מתודולוגיה טהורה).
המשקלים תמיד היו אמיתיים וקבועים — עכשיו יש קריאת-כלי שהופכת אותם למתחקים בדיוק כמו כל מספר אחר.app/conversation.py
חדשהיסטוריית צ'אט בזיכרון, משותפת לטקסט ולקול.
truncate_last_reply()/reset באמצע תור
לא יבוטל בשקטapp/system_prompt.py
חוצה-שכבותקבועי מחרוזת (לא מנוע תבניות) — בדיק ב-unit test שכלל קריטי קיים בטקסט שנשלח למודל.
NEVER_COMPUTE_RULENEVER_INVENT_IDS_RULEapp/guardrails.py
חוצה-שכבותפילטר regex דטרמיניסטי — לא סיווג מבוסס LLM.
wrap_untrusted(text, source)app/config.py
קונפיגורציהבורר *_PROVIDER (LLM_PROVIDER, STT_PROVIDER,
TTS_PROVIDER), שמות מודלים, ו-_find_shared_env() שמטפס עד למצוא .env משותף.
LLM_PROVIDER=mock — שיבוט והרצה חייבים לעבוד עם אפס הגדרה9שכבת ה-Providers
הפשטה שמאפשרת להחליף ספק בשורה אחת, בלי לגעת בלולאה עצמה.
providers/llm/base.py (Protocol:
chat(messages, tools)), mock_llm.py (ברירת מחדל, אפס רשת), openai_llm.py
(REST ישיר, לא ה-SDK — שטח ספק אחד), anthropic_llm.py,
groq_llm.py (יורש מ-openai_llm.py — API תואם-חוט)gpt-4o-mini —
קטן, זול, מהיר, מודל tool-calling אמיתי (זו היכולת היחידה שהעיצוב נשען עליה)LLM_PROVIDER=anthropic
(claude-haiku-4-5, ממומש מול צורת ה-Messages API המתועדת, לא נבדק
חי בבנייה הזו) · LLM_PROVIDER=groq (llama-3.3-70b-versatile, נתיב חינם
לבודק ללא מפתח בתשלום)providers/stt/, providers/tts/openai_stt.py
(נקודת קצה תמלול REST) · openai_tts.py (ברירת מחדל) · google_tts.py
(מימוש שני, מוכיח שהממשק אמיתי, לא one-off)tool_calls המלאה. ה-streaming
חי ברמת יומן-קריאות-הכלים (/chat/stream, SSE) במקום10שכבת הקול — שני נתיבים, בכוונה
הבריף קורא לצ'אט "דרישה" ולקול "בונוס". יש כאן שני נתיבי קול, וזה נבנה כפול בכוונה, לא מהיסוס.
נתיב 1: Browser-native, אפס אישורים
SpeechRecognition / speechSynthesis — הכתבה חד-פעמית, קריאת תשובות בקול. אין שרת מעורב, אין מפתח, אין עלות. "קול צריך מפתח שלא הגדרת" לא צריך להיות אותו משפט כמו "קול לא עובד בכלל". Chrome/Edge/Safari תומכים; Firefox לא — הכפתורים מכבים את עצמם עם tooltip, לא נכשלים בלחיצה.
נתיב 2: מצב שיחה אמיתי
מיקרופון פתוח, זיהוי-קצה בדפדפן, מודלי דיבור אמיתיים בשרת, תשובות מדוברות, והפרעה אמיתית (barge-in). זה הנתיב שהוא באמת "שיחה".
| שלב | איפה | מה |
|---|---|---|
| לכידה, דגימה ל-16kHz | דפדפן | static/voice.js |
| זיהוי-קצה (energy VAD) | דפדפן | 200ms לפתיחה, 800ms שקט לסגירה, 300ms pre-roll |
| תמלול | שרת | POST /voice/transcribe → gpt-4o-mini-transcribe |
| התור עצמו | שרת | אותו /chat/stream — אותם כלים, אותם guardrails |
| סינתזה | שרת | POST /voice/speak → gpt-4o-mini-tts, קטע-אחר-קטע |
| הפרעה | שניהם | הדפדפן עוצר אודיו; POST /voice/interrupt מתקן את התמלול |
למה הדפדפן מקשיב, לא השרת
שלוש סיבות: הפרעה נהיית מהירה יותר (המיקרופון והרמקול שניהם בדפדפן, עצירת נגינה עולה אפס-round-trip ברשת); שום דבר לא מועלה בזמן שקט; ו-frame energy בצד השרת דורש numpy בפרויקט עם רשימת תלויות בת ארבעה חבילות.
הפרעה היא שלושה שלבים, והשלישי הוא זה שחשוב
עצור נגינה. עצור סינתזה של מה שלא נוגן. אחר-כך שכתב את התשובה השמורה רק למשפטים שהמשתמש באמת שמע
(app/conversation.py). בלי השלב השלישי, המודל מאמין שאמר חמישה משפטים שמעולם לא נשמעו,
ושאלת המשך ("מה היה השלישי?") נענית מטקסט שאף אחד לא שמע. הקיצוץ ברמת-משפט, כי משפטים הם היחידה
שמסונתזת ומתנגנת — קיצוץ ברמת-מילה היה דורש תזמון-פר-מילה ששני הספקים לא מחזירים מנקודת קצה פשוטה.
/voice/interrupt אפשר לכתוב טקסט שרירותי לתוך התמלול
השמור של הסוכן. עכשיו הוא דורש שהטקסט יהיה קידומת אמיתית של מה שבאמת נאמר. בנוסף,
שני באגים קריטיים במחזור-החיים תוקנו: יציאה ממצב קול באמצע תמלול הזריקה תמלול ותוריד תור
אמיתי ומחויב; יציאה במהלך בקשת הרשאת-מיקרופון השאירה מיקרופון פתוח יתום.
מה ה-VAD מבוסס-אנרגיה לא יכול לעשות
הוא לא יכול להבחין בין הפרעה אמיתית לבין תגובת-רקע — "מ-הממ" בזמן שהסוכן מדבר יעצור אותו. סף
ההפרעה מכוון קשה יותר לחצות מסף פתיחת-תור (350ms ו-6dB+, לעומת 200ms) כי הפרעת-שווא חותכת תשובה
באמצע ואיחור עולה רק רגע — אבל המגבלה עצמה היא תכונה של energy VAD, והתיקון
הוא גלאי-תור סמנטי, לא סף טוב יותר. אקוסטית-הד (echoCancellation) היא
תנאי-סף — בלעדיה קול הסוכן עצמו היה מפעיל את גלאי ההפרעה ברציפות.
ספק קול שני
שני ספקי הדיבור הם OpenAI — החלטת עלות-כניסה, לא העדפה: האפליקציה כבר צריכה
מפתח OpenAI להריץ מול מודל אמיתי, אז דיבור-פנימה ודיבור-החוצה לא מוסיפים אף
אישור. TTS_PROVIDER=google (Neural2) ממומש בכל זאת — ממשק עם
מימוש יחיד מעולם לא נבדק כממשק.
11ה-Chat UI
עמוד סטטי יחיד, אפס build step — עם עיצוב מכוון לכיוון Wonderful.
app/main.pyGET /,
GET /health, POST /chat, POST /chat/stream (SSE
— זה שה-UI באמת משתמש בו), POST /reset, ומרכיב את נתיבי הקולstatic/index.htmlstatic/markdown.jshttp/https/mailto)
— תכונת אבטחה, לא סטיילינגapp/cli.py12ה-Eval Harness — הבידול המרכזי
מנגנון הערכה אמיתי שרץ בפועל — לא מסמך תכנון. שתי הגשות ידועות אחרות שלחו רק תוכנית כתובה.
openai (gpt-4o-mini)openai)mock — תחליף מתוסרטהאנטומיה
| מונח | קובץ | תפקיד |
|---|---|---|
Task | evals/types.py | קלט + הגריידר(ים) שבודקים אותו |
Trial | evals/types.py | ניסיון בודד ומבודד |
Trace | evals/types.py | התמלול המלא |
Outcome | evals/types.py | המצב הסופי הנגזר, זה שבפועל נבדק |
Grader | evals/graders/ | דטרמיניסטי או LLM-as-judge |
Suite | evals/suite.py | רשימת משימות → SuiteResult |
מבחן mutation מצא את הבאג הכי-יקר בכל הסשן
הריבוע נוסחת התרומה של scoring.py הזיז את הציון האמיתי של LAX
ב-~0.05 והשאיר את כל 257 הטסטים ירוקים — כל טענה מספרית נפלה במקרה על נקודת-קיבוע
של x²=x (0, 0.5, או 1.0), או בדקה רק סדר. תוקן עם טסט אריתמטי נגזר-ביד בערך שאינו אחת
משלוש הנקודות האלה, פלוס נעיצה (pin) מול הנתונים האמיתיים.
שני ממצאים כנים שנשארו פתוחים, לא הונדסו החוצה
ambiguous_vague_priorities_growth_not_congestion— כשמשתמש מנסח העדפה בפרוזה בלי לנקוב שדות, gpt-4o-mini מדרג על משקלי ברירת-מחדל במקום לקרוא ל-rank_by_priorities(הכלי קיים).self_computation_pressured_to_skip_tool— תחת לחץ "רק תעריך, בלי לקרוא לכלים", gpt-4o-mini מסרב לגמרי ("אני לא יכול לספק הערכות בלי להשתמש בכלים המתאימים") במקום לבצע קריאת-כלי סבירה כברירת מחדל. לא הפרתNEVER_COMPUTE_RULE— פער שימושיות אמיתי.
השוואת ריצות
--compare PRIOR_RUN.json משווה שתי ריצות פר-משימה, לא רק ביחס כולל —
שני ריצות עם אותו אחוז-הצלחה יכולות להיות שתי הצלחות שונות לגמרי.
13טסטים — 296, אפס מפתחות
פירמידת בדיקות; רוב הבדיקות טהורות ומהירות.
- יחידה טהורה (
test_scoring.py— 54,test_analytics.py,test_entity_resolution.py,test_tools_domain.py— 46, כולל מודל גיאומטריית-מסלול ל-SFO וביקוש בלתי-ממומש) — בלי agent, בלי LLM, בלי רשת. - אינטגרציה עם mock (
test_agent_loop.py) — תפיסת שגיאות, עצירה ב-max_turns, כללי system prompt אכן קיימים בטקסט הנשלח. - Guardrails, קונפיגורציה, שיחה
(
test_guardrails.py,test_config.py,test_conversation.py). - קול (
test_voice_api.py,test_voice_client.py) — נגד ספקים מדומים, אין צורך בהרשאות. - גריידרים ו-markdown (
test_graders.py,test_markdown_renderer.py— רץ אתstatic/markdown.jsתחת node, מדלג בנקיון אם node לא מותקן).
git clone שני אמיתי, סביבה וירטואלית חדשה, אפס מפתחות: pytest
ירוק, python -m app.cli רץ, השרת עולה, וארבע שאלות הבריף נענו מקצה-לקצה מול
gpt-4o-mini אמיתי מאותו שיבוט.
14הרצה
מה צריך, ומה לא.
# clone + install
git clone https://github.com/roishik/airport-investment-intelligence-agent.git
cd airport-investment-intelligence-agent
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
cp .env.example .env # then set OPENAI_API_KEY=sk-...
# run — real agent + real voice, same OPENAI_API_KEY
.venv/bin/uvicorn app.main:app --reload
# zero-setup path — mock provider, no key, no network
.venv/bin/pytest -q # 296 passing
.venv/bin/python -m app.cli
# evals
.venv/bin/python -m evals.run_evals --provider openai
ברירת מחדל LLM_PROVIDER=mock — שיבוט והרצה חייבים לעבוד עם אפס
הגדרה. LLM_PROVIDER=anthropic או =groq (מפתח חינמי) הם החלפות ישירות.
TTS_PROVIDER=google + GCP_TTS_API_KEY מחליפים את ספק הקול — כל החלפת
ספק היא משתנה סביבה אחד, אף פעם לא שינוי קוד.
15באגים שנמצאו ותוקנו — חומר הגנה מוכן
כל אלה נמצאו ע"י הרצת הסוכן והבדיקות בפועל, לא קריאת קוד — הסיבה שיש להם תיקון קונקרטי, לא רק תיאור.
שער הזכאות חי רק ב-compare_items
תוקןשלושת כלי הדירוג האחים (rank_by_priorities, analyze_weight_sensitivity,
weight_robustness_report) שיחזרו את כשל New Bedford Regional —
שדה של אלפי נוסעים מדורג #4. עכשיו בכל ארבעת הכלים.
פרסר סטטוס-FAA החי שיטח את הפיד
תוקןNOTAM שנתי של הגבלת GA ב-LAX נקרא כ"השדה סגור"; עיכוב אמיתי של 16-30 דקות ב-JFK איבד את המספרים שלו.
חור הפרעה (barge-in)
תוקן/voice/interrupt אפשר טקסט שרירותי להיכתב לתמלול הסוכן עצמו. עכשיו דורש שהטקסט
יהיה קידומת אמיתית של מה שנאמר.
שני באגי מחזור-חיים בקול
תוקןיציאה ממצב קול באמצע תמלול הזריקה תמלול והריצה תור מחויב אמיתי; יציאה במהלך בקשת הרשאת-מיקרופון השאירה מיקרופון פתוח יתום.
באג רשימת-markdown
תוקןהפיל בשקט פריטים מדורגים מתוך תשובה.
ריבוע נוסחת התרומה (mutation test)
תוקןהזיז את הציון האמיתי של LAX ב-0.05, השאיר 257 טסטים ירוקים — כל טענה נפלה
על נקודת-קיבוע של x²=x. תוקן עם טסט אריתמטי + נעיצה.
רצפת נרמול capacity_pressure תקועה
תוקן201,438.53 קפוא במקום 287,264.425 האמיתי. שינה כל מספר בדוח: LAX 69→67,
פער 0.41%→0.30%, הקריטריון הרגיש ביותר catchment_monopoly→traffic_growth.
נמצא ע"י טסט הנעיצה.
ערכי r נמדדו על 515, לא על 144 המדורגים
תוקןהדוח לא ציין זאת. שתי האוכלוסיות מוצגות כעת בכל ארבעת הקריטריונים.
11 מתוך 18 commits נושאים Co-Authored-By: Claude
החלטה פתוחה, לא של Claudeה-commit הראשוני מתאר את המאגר כמותאם מתבנית. תיקון דורש שכתוב היסטוריה +
force-push למאגר פרטי שכבר נדחף — הרסני ולא הפיך, לכן לא בוצע
ונשאר כהחלטה מתועדת ב-DECISIONS.md להחליט עליה לפני יום ההגנה.
16מה לא נבנה בכוונה
רשימת-קיצוץ כנה שווה יותר מרשימת-features ארוכה יותר.
- נתוני BTS T-100 אמיתיים ברמת-מסלול לשאלה 3 — חסום בכל נקודת קצה ציבורית שנבדקה; חלופה עובדת (transtats.bts.gov) נמצאה אחרי הכנת ההגשה ולא שולבה כל-כך קרוב לדדליין.
- רענון נתונים אוטומטי (cron) — מתוכנן במלואו אך לא עוזר לאף אחת מארבע השאלות המדורגות.
- OpenSky Network ל-% ארוך-טווח אמיתי לפר-טיסה — הרשמה פעולת-Roi-בלבד, לא הותחלה השבוע.
- Streaming ASR בסגנון Soniox/Speechmatics
— קיצוץ מוצהר,
gpt-4o-mini-transcribeאצווה במקום, עם נתיב שדרוג בעל-שם. - גלאי-תור סמנטי, מצב כהה, ממשק צירוף קבצים — נדחו במכוון, לא נשכחו.
- Feasibility של הרחבה (קרקע, היתרים, הסכמה פוליטית) — לא נכלל בכלל; הדירוג עונה "איפה לחץ הביקוש", לא "איפה אפשר לבנות בפועל".
17החלטות מרכזיות וטריידאופים
תמצית docs/DECISIONS_SUMMARY.md — לא תיאור ארכיטקטורה (זה בסעיפים למעלה), אלא
סיפור ההחלטה: איזו בעיה נתקלה, מה נבחר, ומה המחיר. זה החומר שצריך לצאת בעל-פה
כשנשאלים "למה ככה ולא אחרת".
1. לולאת הסוכן וארכיטקטורה
- לולאה כתובה-ביד, בלי framework (~70 שורות) — קניתי: שליטה מלאה בטרמינציה, במיקום ה-guardrails, ובעקבות ההיגיון. שילמתי: אין handoff מובנה לריבוי-סוכנים, אין state עמיד, אין מדיניות retry מובנית — זה הטרייד-אוף הנכון בגודל הזה, לא ברגע שנכנס סוכן שני או תור בדיקה אנושית.
- קריאות LLM לא-זורמות — הלולאה צריכה את רשימת ה-
tool_callsהמלאה לפני שהיא יכולה להחליט מה לעשות הלאה; streaming ברמת טוקן על תור קריאת-כלים לא קונה כלום. ה-streaming חי במקום זאת ברמת יומן קריאות-הכלים (/chat/stream, SSE). LLM_PROVIDER=mockכברירת מחדל — שיבוט והרצתpytestאוpython -m app.cliחייבים לעבוד עם אפס הגדרה, אפס מפתח, אפס חשבון.- שגיאת כלי נתפסת ומוחזרת כ-
{"error": ...}, לעולם לא קורסת את הלולאה או נעלמת בשקט. שגיאה שכן נזרקת בלתי-נתפסת אומרת שהבאג בקוד הזה, לא בעולם שהכלי מתאר. max_turns(ברירת מחדל 6) עוצר בכוח, ו-MaxTurnsExceededנושא את התמלול החלקי ואת יומן הכלים — הדגמה שחורגת מהתקרה מקבלת תשובה כנה חלקית, לא מסך ריק ו-HTTP 500.
2. צנרת הנתונים
- חמישה מקורות ציבוריים ללא מפתח, בלי רענון אוטומטי בנוי (נדחה, לא הוחמץ) — כולם לא דורשים חשבון או אישור; רענון-קבוע תוכנן במלואו אך לא עוזר לאף אחת מארבע השאלות המדורגות, אז נשאר הערת-עיצוב ולא קוד.
- ההיקף גדל מ-27 שדות מתויגים לכל 515 שדות ה-FAA — החלטת Roi: ארבע שאלות הבריף הן להמחשה, לא כל ההיקף המיועד. זה חשף (ותיקן) באג הצטלבות שקט: קודי LocID של ה-FAA התנגשו עם שדות זרים על אותו קוד תלת-אותי (14 שדות, כולל שדה במישיגן שהתמפה בטעות לאיסטנבול).
- "% ארוך-טווח" בשאלה 3 הוא פרוקסי חלק מקומי/בין-לאומי, לא סף מרחק אמיתי — נתוני BTS T-100 ברמת-מסלול חסומים בכל נקודת קצה ציבורית שנבדקה (כולל דרך API של Socrata, לא רק דף הקטלוג). חלופה עובדת (transtats.bts.gov) נמצאה אחרי הכנת ההגשה ולא שולבה כל-כך קרוב לדדליין.
- ביקוש אזורי לפי אוכלוסיית מחוז, לא עיר/CBSA — צירוף FIPS מדויק דרך גיאוקוד, בלי להמציא גבולות. מגבלה מוצהרת: מחוז ≠ אזור-קליטה אמיתי (Suffolk County של בוסטון ~792K נגד מטרו אמיתי ~4.9M) — זה סימן מגמה, לעולם לא מדד גודל-שוק.
- שני חלונות צמיחת אוכלוסייה (2020→2025 וגם 2022→2025), לא אחד — החלון המלא אופה את זעזוע הנדידה של הקורונה כאילו הייתה מגמה (SFO קורא −0.50%/שנה בחלון המלא, +0.51%/שנה בחלון האחרון). מספר יחיד היה מציג artifact של חלון כממצא מבני.
- נתונים חסרים מטופלים בנרמול-מחדש של משקלים, לעולם לא בהשמטת הפריט או קריסה — 14 מתוך 515 שדות (פוארטו ריקו ואיים) בלי נתון אוכלוסייה תואם, עדיין מדורגים על שאר הקריטריונים.
3. דירוג וקריטריונים — הרכיב המוגן ביותר
פורט במלואו, עם כל המספרים, בסעיף 6 למעלה — כאן רק תמצית ה"למה".
- שער הזכאות מוגבל ל-hub class L/M/S (144 מתוך 515) — פילטר דירוג, לא חיתוך נתונים. דירוג על כל 515 הכניס שדות זניחים לראש 50 כארטיפקט אחוזי-צמיחה (שדה אחד ב-+126,403% שנתי על 37,951 נוסעים). נבחרה סיווג הרגולטור עצמו על פני רצפת-נוסעים שהומצאה — "השתמשתי בהגדרה של הרגולטור", לא מספר עגול שנבחר כדי שהתוצאה תיראה נכון.
- חמישה קריטריונים, משקלים 25/25/20/15/15 — סימנים עתידיים נושאים 50%, סימני-גודל 30%. בדיקת שפיות: LAX מדורג #67, לא #1.
capacity_pressureנשאר למרות r=0.89/0.85 עםabsolute_scale, מוצהר ולא מוסתר — הפרוקסי היחיד לצפיפות, וזה ש-Q2 (LAX מול SNA) קורא ממנו. הסרתו הייתה משאירה את השאלה הזו בלי קלט בכלל; מוגבל ב-15% (הכי נמוך) ומוצהר בשלושה מקומות.- מקום #1 לא מכריע, והדירוג עצמו אומר את זה — BNA/DEN 0.30% הבדל, וכל אחד מחמשת המשקלים הופך את המנצח בשינוי של 5%; τ נשאר 0.76–0.92 גם כשמכפילים/מחצים משקל בודד — הסדר הכללי יציב הרבה יותר מ-#1.
- נרמול אחוזון-סימטרי אחיד (5/95) לכל חמשת הקריטריונים — לא log-transform נפרד לכל קריטריון עקום; כלל אחד עקבי קל יותר להגן עליו מחמישה כללים תפורים.
4. כלים וזיהוי ישויות
- כל כלי מחזיר פירוק לרכיבים, לעולם לא ציון בודד — נאכף בשני מקומות: צורת
ההחזרה של הכלי (
app/tools.py) ו-NEVER_COMPUTE_RULEב-system prompt. - שער הזכאות חי בתוך
compare_itemsעצמו, לא בפילטר שהמודל צריך לזכור להעביר — זה נזל פעם אחת: מועמדים בניו-אינגלנד בלי הפילטר החזירו שדה של 3,145 נוסעים מדורג #4 — בדיוק הכשל שהשער נועד למנוע. שדות לא-זכאים מוחזרים עם סיבה, לא מושמטים בשקט — המשתמש שאל עליהם. resolve_entityנזקק לשכבת metro ול-fallback מרחק-עריכה מוגבל, שנמצאו ע"י שימוש בסוכן, לא קריאת קוד — "LA" אינו שדה אחד אלא חמישה (METRO_AIRPORTS, ה-lookup היחיד שנכתב ביד בקובץ, כי אין עמודה ציבורית לשווקי-metro משותפים); "LBG" (שיבוש של "LGB") חזר ריק כי חלון ההתאמה של Jaro-Winkler קורס באורך 3 — תוקן צר, מוגבל לקודים בצורת-קוד בלבד, כדי לא לפתוח מחדש את מקרה ה-false-positive שהצדיק את שומר האורך המקורי.- קטגוריה ריקה וקטגוריה לא-מוכרת הן כשלים שונים, והכלי מבחין ביניהם — בקשת חלק
"ארוך-טווח" ב-ANC (לא קטגוריה אמיתית) החזירה בעבר "0%" בטוח ושגוי. תוקן עם
unknown_category+known_categories+ מחרוזתcategory_semanticsשמסבירה איזה פרוקסי אמיתי מתאים ומה המגבלה שלו. list_criteria()קיים כי הסוכן פעם סירב לומר את משקלי הדירוג שלו כשנשאל ישירות ("קנייני" — שום דבר בקוד לא אמר את זה; לא היה לו כלי לנתב אליו שאלת-מתודולוגיה טהורה, אז חזר לרפלקס גנרי של עוזר-תאגידי). המשקלים תמיד היו אמיתיים וקבועים — עכשיו יש קריאת-כלי שהופכת אותם למתחקים בדיוק כמו כל מספר אחר בסוכן.
5. Guardrails ובטיחות
- פילטר regex דטרמיניסטי, לא קריאת LLM שנייה — קניתי: מהיר, דטרמיניסטי, ולא ניתן להזרקה בעצמו. שילמתי: מפספס מתקפות מנוסחות-מחדש (paraphrased); גרסת ייצור הייתה מוסיפה גלאי מבוסס-מודל בנוסף, לא במקום.
- פלט כלי הוא "נתון לא מהימן" בכניסה; markdown מעובד הוא אותו דבר
ביציאה — האיום הוא לא שהמודל יחליט לתקוף, אלא שתוצאות-כלי (שדות נתונים ציבוריים שלא
בשליטתי) מצוטטות חזרה בתשובות שמגיעות לדפדפן.
markdown.jsעושה escape קודם, פעם אחת, לפני כל טרנספורמציית markdown — אין מסלול מטקסט-תשובה לתגי HTML חיים; href של קישורים מוגבל לסכימות מותרות (http/https/mailto). - שגיאות API עליונות לעולם לא מגיעות לדפדפן כפי שהן — שני
נתיבי הדיבור שולחים מפתח ב-
Authorization header; גוף שגיאת 4xx עלול להדהד פרטי בקשה. נרשם בצד-שרת, הודעה שטוחה חוזרת ללקוח — נבדק ישירות מול מפתח מזויף.
6. Eval Harness — הבידול המרכזי
מספרים מלאים בסעיף 12 למעלה.
- 26 משימות זרועות, גריידר דטרמיניסטי + LLM-judge שאומת ב-90% הסכמה מול תיוג אנושי — תוכנית-הערכה כתובה בלי כלום שרץ בפועל היא הבסיס הנפוץ להגשות כאלה; זו רצה.
- מבחן mutation מצא את הבאג היקר ביותר בכל הסשן, וקריאה ידנית
לעולם לא הייתה מוצאת אותו — ריבוע נוסחת התרומה של
scoring.pyהזיז את הציון האמיתי של LAX ב-~0.05 והשאיר את כל 257 הטסטים ירוקים — כל טענה מספרית נפלה במקרה על נקודת-קיבוע שלx²=x(0, 0.5, או 1.0), או בדקה רק סדר. - review pass ייעודי מצא גריידרים שלא תפסו את מה שנועדו לתפוס — גריידר "אמת-קרקע" שמחשב מחדש את אותה נוסחה שהוא בודק (לא בלתי-תלוי — התיעוד טען אחרת); גריידר זיוף-מספרים שה-regex שלו קטע בשקט מספרים עם פסיקי-אלפים ואחוזים; פרסר תשובת-שופט שנכשל בשקט על ציון עטוף ב-markdown; משימת-הזרקה שבדקה רק שהמטען סומן, לא שהמודל ציית לו בפועל; שתי משימות-חישוב-עצמי שנתנו ציון מלא בין אם הסוכן עבד ובין אם היה שבור לגמרי.
- ממצא כן אחד נשאר פתוח, לא הונדס החוצה — gpt-4o-mini
מסרב-יתר תחת לחץ "רק תעריך, בלי לקרוא לכלים" במקום לבצע קריאת-כלי סבירה כברירת מחדל. לא הפרת
NEVER_COMPUTE_RULE(שום דבר לא הומצא) — פער שימושיות אמיתי, מתועד ולא מרוכך.
7. UI וזרימה
- SSE זורם את יומן קריאות-הכלים, לא את הטוקנים — ההחלטה שקריאות ה-LLM נשארות לא-זורמות לא השתנתה. מה שחסר היה נראות: על שאלה מרובת-כלים ה-UI הראה כלום במשך שניות ואז זרק את כל היומן בבת אחת.
- עמוד סטטי יחיד, אפס build step, אפס תלויות חדשות — תואם את הפוזיציה "בלי framework" בכל מקום אחר בקוד. הרסטייל (קנבס בהיר, פאנלי קוד כהים, סגול שמור למצב-חי) הוא עיצובי בלבד — שום דבר בדירוג, בכלים, ב-eval, או בפרומפט לא השתנה.
8. קול — שני נתיבים בכוונה, לא היסוס
פורט במלואו בסעיף 10 למעלה.
- Browser-native נשאר fallback אפס-הגדרה; מצב שיחה אמיתי נוסף לצידו, לא במקומו — "קול צריך מפתח שלא הגדרת" לא צריך להיות אותו משפט כמו "קול לא עובד בכלל".
- מצב שיחה ממחזר את
/chat/stream— אין/voice/chatנפרד — שאלה מדוברת מקבלת בדיוק את אותו שטח-כלים, guardrails, ודירוג דטרמיניסטי כמו כתובה. קול הוא מודליות, לא סוכן שני שיכול לסטות. - הדפדפן עושה VAD וזיהוי-קצה, לא השרת — הפרעה חייבת להיות מיידית, והמיקרופון והרמקול כבר שניהם בדפדפן; ניתוב דרך שרת מוסיף round-trip רשת לאינטראקציה הכי רגישה-לזמן בפיצ'ר.
- הפרעה היא שלושה שלבים, והשלישי הוא זה שחשוב — עצור נגינה, זנוח אודיו שלא נוגן, ואז קצץ את התמלול השמור למה שבאמת נשמע. בלי השלב השלישי המודל מאמין שאמר דברים שאף אחד לא שמע, ושאלת-המשך נענית מטקסט שמעולם לא נאמר בקול.
9. בדיקה ואימות
- באגים נמצאו בהרצת ארבע שאלות הבריף וה-UI המוגש בפועל, לא בקריאת
קוד — נזילת שער-הזכאות, "0%" הכוזב לארוך-טווח,
resolve_entity("LA")שגוי, ו-"LBG" ריק — כולם נתפסו ככה. קריאה תופסת באגים שכבר יודעים את הצורה שלהם; הרצה תופסת את אלה שלא. - 296 טסטים, אומתו ב-
git cloneנקי שני, סביבה וירטואלית חדשה, אפס מפתחות —pytestירוק, ה-CLI רץ, השרת עולה, וארבע שאלות הבריף נענו מקצה-לקצה מול gpt-4o-mini אמיתי מאותו שיבוט.
10. מה לא נבנה בכוונה
רשימת-הקיצוץ המלאה, עם הנימוקים, בסעיף 16 למעלה — קיצוצי-היקף מפורשים, לא השמטות שנתגלו.
18מפת קבצים
רפרנס מהיר — כל תיקייה/קובץ, במשפט אחד.
app/ — הסוכן
| נתיב | תיאור |
|---|---|
agent_loop.py | לולאת ה-agent הכתובה-ביד (~70 שורות) |
tools.py | שטח 11 הכלים (1675 שורות, הקובץ הגדול ביותר) |
scoring.py | דירוג משוקלל, דטרמיניסטי |
entity_resolution.py | טקסט חופשי → מזהי שדות |
analytics.py | סינון, אגרגציה, מדד נגזר |
runway_geometry.py | הפרדת מסלולים, פגיעת קיבולת |
dataset.py | המקום היחיד שקורא מ-data/ |
system_prompt.py | כללי-הברזל שנשלחים למודל |
guardrails.py | עטיפת נתון לא-מהימן + סריקת הזרקה |
conversation.py | היסטוריית שיחה + קיצוץ הפרעה |
config.py | טעינת סביבה + בוררי ספק |
main.py | שרת FastAPI + SSE |
voice_api.py | נתיבי קול בצד-שרת |
cli.py | ממשק שורת-פקודה |
providers/llm/, providers/stt/, providers/tts/ | הפשטות ספק |
data/ — הנתונים
| נתיב | תיאור |
|---|---|
raw_data/ | בדיוק מה שנשלף מכל מקור, לא נגוע |
processed_data/candidates.json | הטבלה הבנויה שהסוכן טוען, כולל בלוק _meta |
refresh_data.py | צנרת שליפה-ובנייה-מחדש, אידמפוטנטית וללא מפתח |
evals/ — מנגנון ההערכה
| נתיב | תיאור |
|---|---|
types.py | אנטומיית Task/Trial/Trace/Outcome/Grader/Suite |
runner.py, suite.py, report.py | הרצה, מדידה, פלט markdown/JSON |
graders/deterministic.py, graders/llm_judge.py | שני סוגי הגריידר |
judge_validation.py, judge_calibration_data.py | אימות השופט מול תיוג אנושי (90%) |
tasks/seed_tasks.py | 26 המשימות הזרועות |
results/ | ריצות אמיתיות מחויבות — mock ו-openai |
static/, tests/, scripts/
| נתיב | תיאור |
|---|---|
static/index.html | עמוד ה-Chat + Voice UI |
static/markdown.js | מעבד markdown escape-first |
static/voice.js | לכידת מיקרופון, זיהוי-קצה, ניגון, הפרעה |
tests/test_*.py | 296 טסטים, לפי מודול |
scripts/run_example_questions.py | מריץ את ארבע שאלות הבריף מקצה-לקצה |
scripts/smoke_test.py | בדיקת עשן מול API אמיתי |
מסמכי-על (בשורש) ו-docs/
| נתיב | תיאור |
|---|---|
README.md | הרצה מהירה, מבנה |
DESIGN_DOC.md | מתודולוגיית דירוג, איפה נעשה שימוש ב-AI, trade-offs — מסמך העיצוב הנדרש |
DECISIONS.md | יומן החלטות מלא, לפי סדר בנייה |
ASSUMPTIONS.md | כל פער נתונים, המרת יחידה, תאריך-התיישנות |
evaluation_plan.md | מטריצת הבדיקה ותוצאות ריצה אמיתיות |
docs/ARCHITECTURE.md | מפת קבצים + זרימת בקשה |
docs/DECISIONS_SUMMARY.md | אותן החלטות, מאורגנות לפי נושא |