מעבדת DIY – RAG Agent

תוכן עניינים

תרגול זה ידריך אתכם בבניית סוכן RAG (Retrieval-Augmented Generation) מלא וישים באמצעות פייתון. סוכן RAG הוא מודל שפה גדול (LLM) שמסוגל לחפש מידע חיצוני (למשל, מסמכי PDF) ולהשתמש בו כ"בסיס ידע" לפני שהוא עונה על שאלות.

 

במקום לענות רק על סמך נתוני האימון שלו, סוכן ה-RAG שלנו יבצע תהליך דו-שלבי:
 

  1. Retrieval (שליפה): הפיכת מסמכים (PDF) לחלקים קטנים (Chunks) וייצוגם כווקטורים (Embeddings).
  2. Generation (יצירה): שליפת הווקטורים הרלוונטיים ביותר לשאלת המשתמש, ושימוש בהם כקונטקסט (Prompt) למודל ה-LLM (נשתמש ב-GPT-4o).

 

זהו תרגול מעשי שישלב ספריות מפתח בעולם ה-AI: PyPDF2, Sentence Transformers, PyTorch, ו-OpenAI.

 

הערה: כאן נבנה את כל הקוד בתוך קובץ אחד בשם rag_agent.py (או כל שם אחר שתבחרו), תוך הוספת מקטעים באופן מצטבר.


הכנה – הקמת הסביבה הוירטואלית

לפני שמתחילים לכתוב קוד, חשוב להקים סביבה וירטואלית מבודדת (venv). סביבה זו מבטיחה שכל הספריות שנתקין (כמו PyTorch ו-OpenAI) יישארו צמודות לפרויקט זה בלבד ולא יתנגשו עם ספריות אחרות במערכת ההפעלה שלכם.

 

  1. יצירת סביבה וירטואלית (.venv)
     

    • ודאו שאתם נמצאים בתיקיית הפרויקט הריקה. הריצו את הפקודה הבאה ליצירת התיקייה .venv (קיצור של virtual environment):
      
      python3 -m venv .venv
                      
    •  

    • הסבר: הפקודה מורה למפרש הפייתון (Python 3) ליצור סביבה וירטואלית בתיקייה מקומית בשם .venv.
  2.  

  3. הפעלת הסביבה הווירטואלית (Activation)
     

    • כדי "להיכנס" לסביבה כך שפקודות pip install יפעלו בתוכה, הריצו את הפקודה המתאימה למערכת ההפעלה שלכם:
    •  

    • עבור Mac / Linux:
      
      source .venv/bin/activate
                      
    •  

    • עבור Windows (Command Prompt):
      
      .venv\Scripts\activate.bat
                      
    •  

    • אימות: ודאו ששם הסביבה מופיע בסוגריים מרובעים בשורת הפקודה שלכם, לדוגמה: (.venv).

1. התקנה, הכנה והגדרות סביבה

בשלב זה נתקין את כל הספריות הנדרשות ונכין את קובצי התצורה של הפרויקט.

 

  1. התקנת ספריות
     

    • התקינו את כל הספריות הנדרשות לפרויקט:
       
      OpenAILocal Models
      
      pip install PyPDF2 sentence-transformers torch openai python-dotenv colorama
                      
      
      pip install PyPDF2 torch sentence-transformers transformers colorama
      

    •  

    • צרו קובץ חדש בשם rag_agent.py.
    •  

    • הכנת קבצים:
      1. צרו תיקייה בשם assets בתוך תיקיית הפרויקט.
      2. מקמו בתוכה קובץ PDF לבחירתכם, המכיל טקסט בנושא מסויים, וישמש כבסיס הידע לסוכן. לא מוצאים? הורידו מכאן את התסריט המלא של הסרט Lion King.
      3. הכנת מודלי השפה (שיחה ו-Embedding):
         

        • במידה ובחרתם לעבוד עם מודלי שיחה של OpenAI, צרו קובץ בשם .env בתיקייה הראשית של הפרויקט והגדירו בו את מפתח ה-API שלכם בפורמט:
           

          
          OPENAI_API_KEY=<YOUR_API_KEY_HERE>
                                  
        • במידה ובחרתם לעבוד עם מודלים לוקאליים, ונדרשת הורדה שלהם מאתר HuggingFace, צרו קובץ חדש בשם getModels.py והדביקו את הקוד הבא (אם המודלים כבר נמצאים אצלכם על המחשב המקומי – צעד זה לא נדרש):
           

          
          from huggingface_hub import snapshot_download
          
          # Download embedding model
          snapshot_download(repo_id="intfloat/multilingual-e5-base", local_dir="./models/multilingual-e5-base")
          
          # Download chat model (from HuggingFace)
          snapshot_download("google/gemma-2b-it", local_dir="./models/gemma-2b-it")
          

          התקינו את הספריה הנדרשת להורדה:
           

          
          pip install huggingface-hub
          

          וכעת הריצו את הקובץ שיצרתם. המתינו להורדת שני המודלים לספריה המקומית models.

  2.  

  3. אימפורטים והגדרות גלובליות
     

    • העתיקו והדביקו את קטע הקוד הבא לראש הקובץ rag_agent.py:
       
      OpenAILocal Models
      
      import PyPDF2
      import re
      import os
      import torch
      import openai
      from sentence_transformers import SentenceTransformer, util
      from dotenv import load_dotenv
      from colorama import Fore, Back, Style, init
      
      # colorama library - enables colored printouts
      init(autoreset=True)
      
      # 1. global vars for the knowledge base
      kb_content = []  # a list of the textual chunks
      kb_embeddings = None # the embeddings
      kb_embeddings_tensor = None # the embeddings tensor
      embedding_model = None # LLM for the embedding 
      
      # 2. global var for conversation memory management (context)
      conversation = []
      
      # 3. loading .env file as environment variables (for OPEN_API_KEY)
      load_dotenv()
      OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
      
      # sanity test for OPENAI KEY
      if not OPENAI_API_KEY:
          raise ValueError("OPENAI_API_KEY is not set in the .env file.")
                      
      
      
      import PyPDF2
      import re
      import os
      import torch
      from sentence_transformers import SentenceTransformer, util
      from transformers import AutoModelForCausalLM, AutoTokenizer
      from colorama import Fore, init
      
      os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
      os.environ["PYTORCH_MPS_HIGH_WATERMARK_RATIO"] = "0.0"
      
      init(autoreset=True)
      
      kb_content = []
      kb_embeddings_tensor = None
      kb_embeddings = None
      embedding_model = None
      conversation = []
      
      CHAT_MODEL_PATH = "./models/gemma-2b-it"
      EMBEDDING_MODEL_PATH = "./models/multilingual-e5-base"
                      

    •  

    • הסבר: קטע זה מייבא את כל הספריות, מאתחל את `colorama` (להדפסה יפה של טקסטים צבעוניים), ומגדיר את המשתנים הגלובליים שיאחסנו את הנתונים המורכבים (כמו טנזור הווקטורים) לאורך חיי התוכנית.

2. פונקציות בסיס: פירוק PDF ויצירת Embeddings

זהו ליבת תהליך ה-RAG – הפיכת מסמכים לטקסט, חלוקתם ליחידות קטנות, והמרתן לווקטורים.

 

  1. פונקציית add_pdf (Chunking)
     

    • הוסיפו את פונקציית פירוק ה-PDF לסוף הקובץ (אין צורך להתעמק בקוד זה, הוא אינו קשור ישירות למהות המעבדה):
      
      def add_pdf(file_path):
          """
          Reads a PDF, extracts text, cleans it, and splits it into chunks (max 1000 chars) 
          by sentence boundaries, then saves it to knowledge_base.txt.
          """
          with open(file_path, 'rb') as pdf_file:
              pdf_reader = PyPDF2.PdfReader(pdf_file)
              num_pages = len(pdf_reader.pages)
              text = ''
              for page_num in range(num_pages):
                  page = pdf_reader.pages[page_num]
                  if page.extract_text():
                      text += page.extract_text() + " "
      
              # Normalization and cleanup
              text = re.sub(r'\s+', ' ', text).strip()
      
              # Split text into chunks by sentences
              sentences = re.split(r'(?<=[.!?]) +', text)
              chunks = []
              current_chunk = ""
              for sentence in sentences:
                  # Respecting a maximum chunk size of ~1000 characters
                  if len(current_chunk) + len(sentence) + 1 < 1000:
                      current_chunk += (sentence + " ").strip()
                  else:
                      chunks.append(current_chunk)
                      current_chunk = sentence + " "
              if current_chunk:
                  chunks.append(current_chunk)
                  
              # Save chunks to a file, each separated by two newlines
              with open("knowledge_base.txt", "w", encoding="utf-8") as kb_file: # Changed to 'w' to overwrite/start fresh
                  for chunk in chunks:
                      kb_file.write(chunk.strip() + "\n\n")
      
              print(f"PDF content split into {len(chunks)} chunks and saved to knowledge_base.txt.")
      
                      
    •  

    • הסבר: פונקציה זו קוראת PDF, מוציאה טקסט, ומבצעת Chunking - חלוקה ל"חתיכות" של עד 1000 תווים, תוך שמירה על גבולות משפט הגיוניים. התוצאה נשמרת בקובץ טקסט בשם knowledge_base.txt.
  2.  

  3. פונקציית create_embeddings
     

    • קראו היטב את פונקציית יצירת הווקטורים ואת ההערות בגוף הפונקציה, והוסיפו אותה לסוף הקובץ:
       
      OpenAILocal Models

      
      def create_embedding():
          """
          Loads the knowledge base, initializes the Sentence Transformer model, 
          and generates vector embeddings for all chunks.
          """
          global kb_embeddings, kb_embeddings_tensor, kb_content, embedding_model
          
          # 1. Load content from the text file
          if os.path.exists("knowledge_base.txt"):
              with open("knowledge_base.txt", "r", encoding='utf-8') as kb_file:
                  # Read lines, filtering out empty ones (due to the "\n\n" separator)
                  kb_content = [line.strip() for line in kb_file if line.strip()]
      
          print(Fore.YELLOW + f"Loaded {len(kb_content)} knowledge base entries.")
          
          if not kb_content:
              print(Fore.RED + "Knowledge base is empty. Cannot create embeddings.")
              return
      
          # 2. Initialize the Embedding Model (S-Transformer)
          print(Fore.YELLOW + "Initializing Sentence Transformer (all-MiniLM-L6-v2)...")
          embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
      
          # 3. Generate Embeddings
          print(Fore.YELLOW + "Now generating embeddings...")
          kb_embeddings = embedding_model.encode(kb_content, show_progress_bar=True)
      
          # 4. Convert to PyTorch Tensor for fast vector operations
          kb_embeddings_tensor = torch.tensor(kb_embeddings)
          print(Fore.GREEN + f"Embeddings generated successfully. Tensor shape: {kb_embeddings_tensor.shape}")
      
                      
      
      def create_embedding():
          """
          Loads the knowledge base from a text file, 
          initializes the Sentence Transformer model,
          and generates vector embeddings for all stored text chunks.
          """
          global kb_embeddings, kb_embeddings_tensor, kb_content, embedding_model
      
          # 1. Load knowledge base content from file (if it exists)
          if os.path.exists("knowledge_base.txt"):
              with open("knowledge_base.txt", "r", encoding='utf-8') as kb_file:
                  # Read all lines as a list of strings (each line = one knowledge chunk)
                  kb_content = kb_file.readlines()
      
          print(f"Loaded {len(kb_content)} knowledge base entries.")
      
          # 2. Initialize the LOCAL embedding model using the specified model path
          print("Now generating embeddings...")
          embedding_model = SentenceTransformer(EMBEDDING_MODEL_PATH)
      
          # 3. Generate embeddings for all chunks (if there’s content)
          kb_embeddings = embedding_model.encode(kb_content) if kb_content else []
      
          # 4. Convert the resulting embeddings to a PyTorch tensor 
          #    for efficient similarity computation (cosine similarity, etc.)
          kb_embeddings_tensor = torch.tensor(kb_embeddings)
      
      
    •  

    • הסבר: פונקציה זו בונה למעשה את בסיס הידע של הסוכן. היא עושה זאת במספר שלבים:
       

      1. טוענת את שברי הטקסט מתוך קובץ knowledge_base.txt
      2. מורידה (אם לא קיים) ומאתחלת את מודל Sentence Transformer (מודל Embedding פופולרי)
      3. ממירה את הטקסט לווקטורים מספריים. אחסון הווקטורים כ-PyTorch Tensor מאפשר חישובים מהירים מאוד.

       
      בסיום פעולה זאת, נקבל שלושה מערכים בעלי גודל זהה:
       

      • kb_content המכיל את שברי (Chunks) הטקסט מתוך הקובץ המקורי
      • kb_embeddings המכיל את הוקטורים שה LLM חישב לכל אחד מה Chunks, לפי הסדר
      • kb_embeddings_tensor המכיל את הוקטורים, בפורמט PyTorch Tensor מהיר ויעיל חישובית

3. רכיבי ה-LLM: ניהול מודל וקונטקסט שיחה

הכנה לשימוש ב-OpenAI API וניהול תפקידי השיחה (System, User, Assistant).

 

  1. פונקציות get_llm ו-reinit_conversation
     

    • קראו היטב את שתי הפונקציות הבאות, והוסיפו אותן לסוף הקובץ:
       
      OpenAILocal Models

      
      def get_llm():
          """Returns the OpenAI client, the model name, and context window size."""
      
          # we'll use model gpt-4o
          return openai.OpenAI(api_key=OPENAI_API_KEY), "gpt-4o", 128*1024
      
      def reinit_conversation():
          """Resets the conversation history and adds the System message."""
      
          global conversation
          conversation = []
      
          # the system Prompt - the "personality" of the agent, role and instruction
          system_message = "You are a helpful assistant that is an expert at extracting the most useful information from a given text."
          conversation.append({"role": "system", "content": system_message})
      
          print(Fore.CYAN + "Conversation history reset. System prompt applied.")
                      
      
      
      chat_model = None
      chat_tokenizer = None
      device = torch.device("cpu")
      
      def get_llm():
          """
          Loads and returns the local chat model and tokenizer.
      
          If they are already loaded, returns them directly.
          Otherwise, initializes both from the specified model path.
          """
      
          global chat_model, chat_tokenizer, device
      
          # 1. Return cached model and tokenizer if they are already loaded
          if chat_model is not None and chat_tokenizer is not None:
              return chat_model, chat_tokenizer
      
          # 2. Load tokenizer from local model directory
          print("Loading model...")
          chat_tokenizer = AutoTokenizer.from_pretrained(
              CHAT_MODEL_PATH,
              local_files_only=True  # load from disk, no internet access
          )
      
          # 3. Load model weights from the same path (inference mode)
          chat_model = AutoModelForCausalLM.from_pretrained(
              CHAT_MODEL_PATH,
              torch_dtype=torch.float32,  # use standard precision
              device_map=None,            # run on CPU
              local_files_only=True       # disable online downloads
          )
      
          # 4. Set the model to evaluation mode (no gradient tracking)
          chat_model.eval()
      
          print("Model loaded successfully.\n")
      
          # 5. Return the ready-to-use model and tokenizer
          return chat_model, chat_tokenizer
      
      def reinit_conversation():
          """Resets the conversation history and adds the System message."""
      
          global conversation
          conversation = []
      
          # the system Prompt - the "personality" of the agent, role and instruction
          system_message = "You are a helpful assistant that is an expert at extracting the most useful information from a given text."
          conversation.append({"role": "system", "content": system_message})
      
          print(Fore.CYAN + "Conversation history reset. System prompt applied.")
                      

    •  

    • הסבר: הפונקציות האלה מכינות את המסגרת לשיחה: get_llm מחזירה את הקליינט של OpenAI, ו-reinit_conversation מנקה את היסטוריית השיחה (בדיוק כמו כפתור "New Chat") ומכניסה את ה-System Prompt שנותן למודל את ההנחיות והאישיות שלו.

4. מנוע השליפה (Retrieval)

הבסיס ל-RAG – חישוב וקטור השאלה והשוואתו לכל הווקטורים בבסיס הידע.

 

  1. פונקציית get_relevant_context
     

    • קראו היטב והוסיפו את פונקציית החיפוש הווקטורי:
      OpenAILocal Models
      
      def get_relevant_context(user_input, kb_embeddings, kb_content, embedding_model, top_k=3):
          """
          Finds the most relevant chunks from the knowledge base using vector similarity.
          """
      
          if kb_embeddings.nelement() == 0: # Check if the tensor has any elements
              return []
      
          # 1. Encode the user input into a vector
          input_embedding = embedding_model.encode([user_input], convert_to_tensor=True)
      
          # 2. Compute Cosine Similarity between the input and all kb embeddings
          # Cosine Similarity returns similarity ranks between 1 (very similar) to 0 (irrelevant)
          cos_scores = util.cos_sim(input_embedding, kb_embeddings)[0]
      
          # 3. Get the indices of the top-k scores
          top_k = min(top_k, len(cos_scores))
          top_indices = torch.topk(cos_scores, k=top_k)[1].tolist()
      
          # 4. Return the corresponding text chunks
          relevant_context = [kb_content[idx].strip() for idx in top_indices]
          return relevant_context
                      
      
      def get_relevant_context(user_input, kb_embeddings, kb_content, embedding_model, top_k=3):
          """
          Finds and returns the most relevant text chunks from the knowledge base
          using cosine similarity between the user input and precomputed embeddings.
          """
      
          # 1. Check if the embeddings tensor is empty (no knowledge base loaded)
          if kb_embeddings.nelement() == 0:
              return []
      
          # 2. Encode the user input into a numerical vector (embedding)
          input_embedding = embedding_model.encode([user_input])
      
          # 3. Calculate cosine similarity between the input vector and all stored embeddings
          #    Cosine similarity measures how close two vectors are in direction (1 = identical, 0 = unrelated)
          cos_scores = util.cos_sim(input_embedding, kb_embeddings)[0]
      
          # 4. Extract indices of the top-k most similar embeddings
          top_indices = torch.topk(cos_scores, k=min(top_k, len(cos_scores)))[1].tolist()
      
          # 5. Return the matching text chunks (stripped of extra spaces)
          return [kb_content[i].strip() for i in top_indices]
                      
    •  

    • הסבר: פונקציה זו היא הלב של ה-RAG, והיא עושה את הפעולות הבאות:
       

      1. לוקחת את שאלת המשתמש והופכת אותה לווקטור
      2. משווה אותה לכל הווקטורים של ה-Chunks (חישוב Cosine Similarity)
      3. מחזירה את הטקסט של שלושת ה-Chunks (ברירת המחדל) שהיו הכי קרובים לשאלה

5. פונקציית ה-Generation והשיחה (Chat Loop)

שילוב הקונטקסט שנשלף לתוך הפרומפט הסופי ושליחתו למודל ה-LLM.

 

  1. פונקציית `llm_chat`
     

    • קראו היטב והוסיפו את הפונקציה המרכזית שמנהלת את השיחה:
      OpenAILocal Models
      
      def llm_chat(user_input, kb_embeddings, kb_content, embedding_model):
          """
          Manages the RAG process: Retrieves context, builds the prompt, 
          sends to the LLM, and updates conversation history.
          """
      
          global conversation
          
          # 1. Retrieval: Get relevant context
          relevant_context = get_relevant_context(user_input, kb_embeddings, kb_content, embedding_model)
          
          context_str = ""
          if relevant_context:
              context_str = "\n\n".join(relevant_context)
              print(Fore.GREEN + "\n--- 📚 Context Pulled from Documents (RAG) ---")
              print(context_str)
              print("------------------------------------------\n")
          else:
              print(Fore.RED + "No relevant context found. Answering based on general knowledge or conversation history.")
      
          # 2. Prompt Construction: Add context and strict instructions
          user_input_with_context = user_input
          if relevant_context:
              # the relevant context is concatenated to the beginning of the user prompt (ICL)
              user_input_with_context = "CONTEXT:\n" + context_str + "\n\nUSER QUESTION:\n" + user_input
      
          # hard instructions to the model (Guardrails) - this instructs the model to respond ONLY according to the provided context
          user_input_with_context += """\n\n
          INSTRUCTIONS:\n
          1. Do not use your internal training data to answer.\n
          2. If you do not find the answer in the given CONTEXT or in our conversation history then
          you must answer this exact phrase: 'I do not know' """
      
          conversation.append({"role": "user", "content": user_input_with_context})
      
          # 3. Generation: Send to the LLM
          brain, chat_model, _ = get_llm()
      
          try:
              response_obj = brain.chat.completions.create(
                  model=chat_model,
                  messages=conversation,
                  timeout=30 # time limit of 30 seconds
              )
              
              # 4. Process response and update history
              response = response_obj.choices[0].message.content
              conversation.append({"role": "assistant", "content": response})
      
              return response
          
          except openai.APIError as e:
              return f"OpenAI API Error: {e}"
          except Exception as e:
              return f"An unexpected error occurred: {e}"
      
                      
      
      
      def llm_chat(user_input):
          global conversation
      
          model, tokenizer = get_llm()
      
          relevant_context = get_relevant_context(user_input, kb_embeddings_tensor, kb_content, embedding_model)
          if relevant_context:
              context_str = "\n".join(relevant_context)
              print(Fore.GREEN + "Context Pulled:\n" + context_str[:1000] + "\n")
          else:
              context_str = ""
              print("No relevant context found.")
      
          user_input_with_context = (
              f"Use only the following context:\n{context_str}\n\n"
              f"Question: {user_input}\n"
              f"Answer only based on the context. "
              f"If no relevant info, say exactly: I do not know."
          )
      
          conversation.append({"role": "user", "content": user_input_with_context})
          prompt = build_prompt(conversation)
      
          inputs = tokenizer(prompt, return_tensors="pt").to(device)
      
          with torch.no_grad():
              outputs = model.generate(
                  **inputs,
                  max_new_tokens=250,
                  temperature=0.6,
                  do_sample=True,
                  pad_token_id=tokenizer.eos_token_id,
                  eos_token_id=tokenizer.eos_token_id
              )
      
          text = tokenizer.decode(outputs[0], skip_special_tokens=True)
          answer = text.split("[Assistant]")[-1].strip()
          conversation.append({"role": "assistant", "content": answer})
          return answer
                      
    •  

    • הסבר: פונקציה זו מארגנת את כל התהליך: היא משתמשת ב-get_relevant_context לשליפה, בונה את ה-Prompt המועשר (כולל ה-Guardrails שמורים למודל לענות רק על סמך המידע שקיבל), שולחת ל-OpenAI, ושומרת את התשובה בהיסטוריית השיחה.
  2.  

  3. לולאת השיחה והרצה ראשית
     

    • קראו היטב והוסיפו את הפונקציה המנהלת את לולאת הקלט/פלט ואת בלוק ההרצה הראשי (שימו לב שנו את נתיב קובץ ה PDF לטעינה בהתאם):
       
      OpenAILocalModels
      
      def startChat():
          """Main chat loop for user interaction."""
          reinit_conversation()
      
          print(Fore.MAGENTA + "Welcome to the DIY RAG Agent chat!")
          print(Fore.MAGENTA + "Type '/reset' to clear history, '/exit' to quit.")
      
          while True:
              try:
                  user_input = input(Fore.BLUE + "\n>>> Ask a question about the document: " + Style.RESET_ALL)
              except EOFError:
                  print("\nExiting chat...")
                  break
              
              if not user_input.strip():
                  continue
      
              # Hard slash commands for resetting the conversation
              if user_input.lower() in ["/reset", "/init", "/restart"]:
                  print(Fore.YELLOW + "Reseting conversation...")
                  reinit_conversation()
                  continue
      
              # Hard slash commands for exiting
              if user_input.lower() in ["/bye", "/exit", "/end"]:
                  print(Fore.MAGENTA + "Exiting the chat. Goodbye!")
                  break
      
              # Call the RAG engine
              response = llm_chat(user_input, kb_embeddings_tensor, kb_content, embedding_model)
              
              print(Fore.CYAN + "\n<<< RAG Agent Response:")
              print(response)
              print("-" * 50)
      
      # --- Main Execution Block ---
      if __name__ == "__main__":
          
          # 1. Load knowledge base if it doesn't exist
          if not os.path.exists("knowledge_base.txt"):
              print(Fore.RED + "knowledge_base.txt not found. Attempting to parse default PDF...")
              
              # assuming the "Lion King Script.pdf" file is in the assets folder
              file_path = "./assets/Lion King Script.pdf"
              
              if os.path.exists(file_path):
                  add_pdf(file_path)
              else:
                  print(Fore.RED + f"ERROR: PDF file not found at {file_path}. Please place a PDF there or create 'knowledge_base.txt' manually.")
                  exit()
      
          # 2. Create Embeddings
          create_embedding()
      
          # 3. Start the chat loop
          if kb_embeddings_tensor is not None and kb_embeddings_tensor.nelement() > 0:
              startChat()
          else:
              print(Fore.RED + "Could not start chat: Embeddings failed to load.")
                      
      
      # ========== Chat Loop ==========
      def startChat():
          """
          Main chat loop that handles user interaction with the RAG system.
          It waits for user input, processes commands, and returns responses from the LLM.
          """
          # 1. Initialize or reset the conversation at the start
          reinit_conversation()
      
          # 2. Continuous loop for user questions
          while True:
              user_input = input("Ask a question about my knowledge: ")
      
              # 3. Command: reset or restart conversation
              if user_input.lower() in ["/reset", "/init", "/restart"]:
                  print("Resetting conversation...")
                  reinit_conversation()
                  continue
      
              # 4. Command: exit the chat
              if user_input.lower() in ["/bye", "/exit", "/end"]:
                  print("Exiting the chat. Goodbye!")
                  break
      
              # 5. Generate a response using the LLM with context retrieval
              response = llm_chat(user_input)
      
              # 6. Display the model's response clearly in the console
              print("llm Response:\n\n" + response + "\n")
      
      
      # ========== Main ==========
      if __name__ == "__main__":
          """
          Main entry point for running the RAG application.
          Ensures that a knowledge base exists, creates embeddings, and starts the chat.
          """
      
          # 1. Load or create the knowledge base
          if not os.path.exists("knowledge_base.txt"):
              file_path = "./assets/Lion King Script.pdf"
              add_pdf(file_path)  # Parse and append PDF content to knowledge base
      
          # 2. Generate embeddings from the knowledge base
          create_embedding()
      
          # 3. Launch the interactive chat loop
          startChat()
      
                      
    •  

    • הרצה: כעת הקובץ מוכן להרצה! ודאו שהסביבה הווירטואלית פעילה, וכתבו בטרמינל:
      
      python rag_agent.py
                      
    • שימו לב: אם השתמשתם במודל שיחה לוקאלי - זהו מודל גדול משמעותית ממודל ה Embedding, ובשימוש הראשון בו בהרצה הנוכחית - לוקח לו זמן להטען לזיכרון ולהיות זמין ל inference. העזרו בסבלנות.

לאחר הרצת סוכן ה-RAG, אתם צפויים לראות בטרמינל שלושה סוגי פלט מרכזיים המדגימים את תהליך ה-RAG במלואו:
 

  1. שלב האתחול: בתחילה תראו הודעות המאשרות את טעינת בסיס הידע, הורדת מודל ה-Embedding (אם לא עבדתם עם מודל לוקאלי) ויצירת הווקטורים.
  2. שליפת הקונטקסט (Retrieval): בכל שאלה שתשאלו, תופיע הדפסה ירוקה גדולה תחת הכותרת --- Context Pulled from Documents (RAG) ---. זוהי ההוכחה שהסוכן חישב את הדמיון הווקטורי, שלף את ה-Chunks הרלוונטיים ביותר מה-PDF, וצירף אותם ל-Prompt.
  3. תשובת המודל (Generation): לאחר מכן תופיע תגובת ה-LLM תחת <<< RAG Agent Response. התשובה הזו תהיה מבוססת באופן בלעדי על הקונטקסט הירוק שסופק לו, וזהו הדבר החשוב ביותר שלמדנו כאן: אנו שולטים במקור המידע של המודל.

למדנו לבנות ארכיטקטורת AI מודרנית המשלבת עיבוד שפה טבעית (NLP) עם יכולות מתקדמות של LLMs, ובכך הפכנו את המודל ל"מומחה" במידע הספציפי שהענקנו לו.