:summary: שיחה עם מודל שפה שעונה אם CodeKeeper מתאים לאופן העבודה של הגולש: כניסה ויציאה, ניתוב שלא מגיע לשמירת קוד, זיכרון השיחה והמכסה היומית במונגו, וההגדרות. .. _advisor: מצב היועץ (/advisor) ==================== היועץ עונה לגולשים, ובעיקר למי שעוד לא משתמש ב-CodeKeeper, אם המוצר מתאים לאופן שבו הם עובדים עם סוכני AI, או שפתרון פשוט יותר יספיק להם. הוא עונה מתוך מסמך ידע אחד, ``services/advisor_knowledge/codekeeper.md``, שנכנס כולו לפרומפט בכל שאלה, בלי RAG. הוא עבר לכאן מהבוט הנפרד ``codekeeper_advisor_bot``. המדריך המלא, עם הנימוק לכל החלטה בבנייה, נשאר בריפו ההוא (``docs/codekeeper_advisor_bot_guide.md``) וב-CodeKeeper. העמוד הזה מתאר את ההשתלבות בבוט. כניסה ויציאה ------------- - **כניסה:** הפקודה ``/advisor``, או הקישור ``https://t.me/my_code_keeper_bot?start=advisor``, שמגיע לבוט כ-``/start advisor``. גולש שמעולם לא פתח את הבוט ונכנס מהקישור מקבל את פתיחת היועץ, ולא את הפתיחה הרגילה. הכניסה אינה רושמת אותו כמשתמש, ואת זה עושה ``/start`` הרגיל, שהודעת היציאה מפנה אליו. אין ליועץ כפתור בתפריט הראשי. - **יציאה:** כפתור "יציאה מהיועץ" שמופיע בכל תשובה, או הפקודה ``/cancel``. לחיצה על כפתור יציאה בהודעה ישנה, אחרי שהגולש כבר יצא, עונה שהוא כבר בחוץ ואיך חוזרים. אין יציאה אוטומטית אחרי זמן בלי פעילות: גולש שחוזר אחרי שעה ממשיך את השיחה מאיפה שעצר. המחיר הוא שגולש ששכח שהוא ביועץ ושולח קוד מקבל תשובה מהמודל ולא שמירה, עד שהוא יוצא. - **כל פעולה אחרת מוציאה מהיועץ:** פקודה, כפתור של המקלדת הראשית, קובץ, או לחיצה על כפתור של תפריט אחר. הגולש מקבל הודעה קצרה שהיועץ נסגר, והפעולה ממשיכה לבוט הרגיל כאילו לא היה שם. היועץ עונה על טקסט בלבד. עריכה של הודעה בזמן שהיועץ פתוח נבלעת: היא אינה שאלה חדשה, וגם אינה קוד לשמירה. הניתוב ------- מחוץ ליועץ, כל הודעת טקסט שלא שייכת לזרימה אחרת היא קוד לשמירה (``handle_text_message`` ב-``main.py``). לכן היועץ הוא ``ConversationHandler`` בקבוצה משלו, ``ADVISOR_HANDLER_GROUP`` ב-``handlers/advisor.py``: אחרי שער מגבלת הקצב, ולפני כל handler טקסט אחר של הבוט. הודעה שהיועץ לוקח עוצרת שם ב-``ApplicationHandlerStop``, ולא מגיעה לשאר הקבוצות. הבוט מעבד עדכון אחד בכל פעם, וקריאה למודל נמשכת שניות. לכן ה-handlers של היועץ רק מחליטים לאן ההודעה הולכת ומחזירים מיד, והעבודה עצמה (הפתיחה, הקריאה למודל, השליחה והשמירה) רצה במשימה של ``application.create_task``. את ההחלטה אי אפשר להעביר לרקע: ב-handler שרץ עם ``block=False``, ``ApplicationHandlerStop`` לא עוצר דבר (python-telegram-bot 22.5, ``telegram/ext/_application.py``), וההודעה הייתה ממשיכה גם לשמירת קוד. כל המשימות של גולש אחד עוברות דרך ``asyncio.Lock`` אחד, ולכן שאלה ששולחים מיד אחרי הכניסה רואה את הפתיחה בזיכרון השיחה, ושתי שאלות מהירות נענות לפי הסדר. .. literalinclude:: ../handlers/advisor.py :language: python :pyobject: build_advisor_conversation :caption: handlers/advisor.py – build_advisor_conversation **מה לא שורד ריסטארט:** מצב השיחה חי בזיכרון, כמו בכל שאר הזרימות של הבוט, שאין לו persistence. אחרי פריסה הגולש כבר מחוץ ליועץ, וההודעה הבאה שלו מגיעה לבוט הרגיל. זיכרון השיחה והמכסה היומית שמורים במסד, ולכן הם נשמרים. מה נשמר במסד ------------- - ``advisor_history`` **— זיכרון השיחה.** מסמך אחד לכל זוג שאלה-תשובה, כך ש-``insert_one`` אחד שומר את שתיהן או אף אחת. נשמרות רק תשובות מוצלחות, והפתיחה נשמרת כזוג הראשון, כי היא שואלת את הגולש שאלות. ``ADVISOR_CONTEXT_EXCHANGES`` הזוגות האחרונים נכנסים לפרומפט (ראו בהמשך, "הגדרות והקריאה למודל"). המסמכים נמחקים באינדקס TTL אחרי ``HISTORY_RETENTION_DAYS`` ימים, וזו הבטחה שהפתיחה נותנת לגולש. השאילתה מסננת לפי גיל גם בעצמה, כי מחיקת ה-TTL אינה מיידית. - ``advisor_daily_usage`` **— המכסה היומית.** מגבלת הקצב של הבוט סופרת הודעות בדקה, כלומר עוצרת הצפה ולא עלות. לכן לכל גולש יש תקרה של ``DAILY_QUESTION_LIMIT`` שאלות ליום, לפי היום בשעון ישראל, ולחיצה על שאלת המשך נספרת כשאלה. הבדיקה והרישום הם פעולה אטומית אחת (``take_daily_question``), שעומדת גם בבקשות מקבילות. בלי מסד היועץ אינו עונה: אי אפשר לספור, ולכן גם לא לאשר. שמות האוספים, ``HISTORY_RETENTION_DAYS`` ומפרט האינדקסים יושבים ב-``advisor_collections.py``, מודול שורש בלי pymongo, כמו ``mcp_uploads.py``: ``database/manager.py`` מייבא אותו, וחייב לעלות גם כשהייבוא של pymongo נכשל. חלון השמירה קבוע בקוד ולא משתנה סביבה, כי כל שירות שמייבא את ``database`` מצהיר על אינדקס ה-TTL בעלייה, וערך שמוגדר רק בבוט היה מתהפך בכל עלייה של שירות אחר. ``DAILY_QUESTION_LIMIT`` והקריאה והכתיבה עצמן ב-``services/advisor_store.py``. האינדקסים נוצרים בכל עלייה ב-``DatabaseManager._create_advisor_indexes``, וכשל של אינדקס TTL יוצא כאירוע ``db_advisor_index_missing`` ברמת error (:doc:`observability/events_catalog`). הגדרות והקריאה למודל --------------------- חמישה משתני סביבה בשירות של הבוט (:doc:`environment-variables`): ``ADVISOR_API_KEY``, ``ADVISOR_BASE_URL`` ו-``ADVISOR_MODEL`` הם חובה. ``ADVISOR_CONTACT_URL`` הוא הקישור לכפתור "לדבר עם המפתח". ``ADVISOR_CONTEXT_EXCHANGES`` הוא אורך הזיכרון, בזוגות של שאלה ותשובה, ובלעדיו ``DEFAULT_CONTEXT_EXCHANGES``. הוא נספר בזוגות ולא בהודעות כמו ``CONTEXT_WINDOW_SIZE`` בבוט הנפרד, כי כל מסמך בזיכרון השיחה הוא זוג. כל זוג נשלח שוב בכל שאלה שאחריו, ולכן ``MAX_CONTEXT_EXCHANGES`` חוסם ערך שהוקלד בטעות. כשאחד מהשלושה חסר או פגום, היועץ כבוי: ``/advisor`` עונה שהוא לא זמין, ``/start advisor`` מגיע ל-``/start`` הרגיל, ולחיצה על כפתור בהודעה ישנה של היועץ, מתקופה שבה היה פעיל, עונה שהוא לא זמין (ה-handler הכללי של הלחיצות לא מכיר את הכפתורים האלה ולא עונה עליהם). הסיבה נרשמת בלוג בעלייה, והבוט עצמו עולה כרגיל. הקריאה למודל עוברת דרך ``openai.AsyncOpenAI`` מול כל API שתואם OpenAI, למשל Gemini. ``base_url`` והמפתח מועברים ללקוח תמיד במפורש, כי בלעדיהם ה-SDK קורא בעצמו את ``OPENAI_BASE_URL`` ואת ``OPENAI_API_KEY`` מהסביבה. לקריאה כולה, כולל הניסיון החוזר של ה-SDK, יש תקרת זמן אחת, ``ANSWER_DEADLINE_SECONDS``. כשל של הספק או חריגה מהתקרה מחזירים לגולש הודעת תקלה כללית, והיא לא נכנסת לזיכרון השיחה. ההגדרות והקבועים ב-``services/advisor_service.py``. בדיקות ------ - ``tests/test_advisor_routing_real_ptb.py`` — הניתוב באפליקציה האמיתית של הבוט, מול python-telegram-bot האמיתי, בתת-תהליך: גולש חדש מהקישור, יציאה בכל הדרכים, הודעות שמגיעות לשמירת קוד אחרי היציאה, והמכסה המלאה. - ``tests/test_advisor_store_mongo.py`` — המכסה תחת בקשות מקבילות, הזיכרון והאינדקסים, מול מונגו אמיתי (ראו :doc:`testing`, הסעיף על בדיקות מול מונגו אמיתי). - ``tests/test_advisor_pipeline.py``, ``tests/test_advisor_service.py``, ``tests/test_advisor_store.py`` ו-``tests/test_advisor_text.py`` — הצינור, הקריאה למודל (כולל בקשה אמיתית של ה-SDK מול שרת מקומי), והעיצוב לטלגרם.