From 7edfba7dca0156cee124ab1409a184647fdcb468 Mon Sep 17 00:00:00 2001 From: Simon <134164156+simondb94@users.noreply.github.com> Date: Fri, 6 Jun 2025 03:27:08 +0100 Subject: [PATCH] Add files via upload Improvements made. --- .../simondb94-Improved-LLM-Tutor-.ipynb | 1449 +++++++++++++++++ 1 file changed, 1449 insertions(+) create mode 100644 week1/community-contributions/simondb94-Improved-LLM-Tutor-.ipynb diff --git a/week1/community-contributions/simondb94-Improved-LLM-Tutor-.ipynb b/week1/community-contributions/simondb94-Improved-LLM-Tutor-.ipynb new file mode 100644 index 0000000..dab89a9 --- /dev/null +++ b/week1/community-contributions/simondb94-Improved-LLM-Tutor-.ipynb @@ -0,0 +1,1449 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fe12c203-e6a6-452c-a655-afb8a03a4ff5", + "metadata": {}, + "source": [ + "Improved-LLM-Tutor" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c1070317-3ed9-4659-abe3-828943230e03", + "metadata": {}, + "outputs": [], + "source": [ + "# Standard library imports\n", + "import os\n", + "import time\n", + "import json\n", + "from typing import Dict, List, Any, Optional, Union, Callable\n", + "\n", + "# Third-party imports\n", + "from dotenv import load_dotenv\n", + "from IPython.display import Markdown, display, HTML, update_display\n", + "from openai import OpenAI\n", + "import ollama\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Try to import rich, install if not available\n", + "try:\n", + " from rich.console import Console\n", + " from rich.markdown import Markdown as RichMarkdown\n", + " from rich.panel import Panel\n", + "except ImportError:\n", + " !pip install rich\n", + " from rich.console import Console\n", + " from rich.markdown import Markdown as RichMarkdown\n", + " from rich.panel import Panel\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4a456906-915a-4bfd-bb9d-57e505c5093f", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Constants\n", + "MODEL_GPT = 'gpt-4o-mini'\n", + "MODEL_LLAMA = 'llama3.2'\n", + "DEFAULT_SYSTEM_PROMPT = \"You are a helpful technical tutor who answers questions about python code, software engineering, data science and LLMs\"\n", + "\n", + "# Set up environment\n", + "load_dotenv()\n", + "openai = OpenAI()\n", + "console = Console()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "a8d7923c-5f28-4c30-8556-342d7c8497c1", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "class LLMTutor:\n", + " \"\"\"\n", + " A class that provides tutoring functionality using multiple LLM models.\n", + " \"\"\"\n", + " \n", + " def __init__(self, \n", + " system_prompt: str = DEFAULT_SYSTEM_PROMPT,\n", + " gpt_model: str = MODEL_GPT,\n", + " llama_model: str = MODEL_LLAMA):\n", + " \"\"\"\n", + " Initialize the LLM Tutor with specified models and system prompt.\n", + " \n", + " Args:\n", + " system_prompt: The system prompt to use for the LLMs\n", + " gpt_model: The OpenAI GPT model to use\n", + " llama_model: The Ollama model to use\n", + " \"\"\"\n", + " self.system_prompt = system_prompt\n", + " self.gpt_model = gpt_model\n", + " self.llama_model = llama_model\n", + " self.history: List[Dict[str, Any]] = []\n", + " self.response_times = {'gpt': [], 'llama': []}\n", + " \n", + " def format_question(self, question: str) -> str:\n", + " \"\"\"\n", + " Format the user's question with a standard prefix.\n", + " \n", + " Args:\n", + " question: The user's question\n", + " \n", + " Returns:\n", + " Formatted question with prefix\n", + " \"\"\"\n", + " return f\"Please give a detailed explanation to the following question: {question}\"\n", + " \n", + " def create_messages(self, question: str) -> List[Dict[str, str]]:\n", + " \"\"\"\n", + " Create the message structure for LLM API calls.\n", + " \n", + " Args:\n", + " question: The user's question\n", + " \n", + " Returns:\n", + " List of message dictionaries\n", + " \"\"\"\n", + " formatted_question = self.format_question(question)\n", + " return [\n", + " {\"role\": \"system\", \"content\": self.system_prompt},\n", + " {\"role\": \"user\", \"content\": formatted_question}\n", + " ]\n", + " \n", + " def get_gpt_response(self, \n", + " question: str, \n", + " stream: bool = True) -> str:\n", + " \"\"\"\n", + " Get a response from the GPT model.\n", + " \n", + " Args:\n", + " question: The user's question\n", + " stream: Whether to stream the response\n", + " \n", + " Returns:\n", + " The model's response as a string\n", + " \"\"\"\n", + " messages = self.create_messages(question)\n", + " start_time = time.time()\n", + " \n", + " try:\n", + " if stream:\n", + " return self._stream_gpt_response(messages)\n", + " else:\n", + " response = openai.chat.completions.create(\n", + " model=self.gpt_model, \n", + " messages=messages\n", + " )\n", + " elapsed = time.time() - start_time\n", + " self.response_times['gpt'].append(elapsed)\n", + " return response.choices[0].message.content\n", + " except Exception as e:\n", + " console.print(f\"[bold red]Error with GPT model:[/bold red] {str(e)}\")\n", + " return f\"Error: {str(e)}\"\n", + " \n", + " def _stream_gpt_response(self, messages: List[Dict[str, str]]) -> str:\n", + " \"\"\"\n", + " Stream a response from the GPT model.\n", + " \n", + " Args:\n", + " messages: The messages to send to the model\n", + " \n", + " Returns:\n", + " The complete response as a string\n", + " \"\"\"\n", + " start_time = time.time()\n", + " try:\n", + " stream = openai.chat.completions.create(\n", + " model=self.gpt_model, \n", + " messages=messages,\n", + " stream=True\n", + " )\n", + " \n", + " response = \"\"\n", + " display_handle = display(Markdown(\"\"), display_id=True)\n", + " \n", + " for chunk in stream:\n", + " delta_content = chunk.choices[0].delta.content or ''\n", + " response += delta_content\n", + " # Clean the response for display\n", + " clean_response = response.replace(\"```python\", \"```\").replace(\"```\", \"\")\n", + " update_display(Markdown(clean_response), display_id=display_handle.display_id)\n", + " \n", + " elapsed = time.time() - start_time\n", + " self.response_times['gpt'].append(elapsed)\n", + " return response\n", + " except Exception as e:\n", + " console.print(f\"[bold red]Error streaming GPT response:[/bold red] {str(e)}\")\n", + " return f\"Error: {str(e)}\"\n", + " \n", + " def get_llama_response(self, question: str) -> str:\n", + " \"\"\"\n", + " Get a response from the Llama model.\n", + " \n", + " Args:\n", + " question: The user's question\n", + " \n", + " Returns:\n", + " The model's response as a string\n", + " \"\"\"\n", + " messages = self.create_messages(question)\n", + " start_time = time.time()\n", + " \n", + " try:\n", + " response = ollama.chat(model=self.llama_model, messages=messages)\n", + " elapsed = time.time() - start_time\n", + " self.response_times['llama'].append(elapsed)\n", + " return response['message']['content']\n", + " except Exception as e:\n", + " console.print(f\"[bold red]Error with Llama model:[/bold red] {str(e)}\")\n", + " return f\"Error: {str(e)}\"\n", + " \n", + " def ask(self, question: str, models: List[str] = ['gpt', 'llama']) -> Dict[str, str]:\n", + " \"\"\"\n", + " Ask a question to one or more models.\n", + " \n", + " Args:\n", + " question: The user's question\n", + " models: List of models to query ('gpt', 'llama', or both)\n", + " \n", + " Returns:\n", + " Dictionary with model responses\n", + " \"\"\"\n", + " responses = {}\n", + " \n", + " # Store the question in history\n", + " self.history.append({\n", + " 'question': question,\n", + " 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),\n", + " 'responses': {}\n", + " })\n", + " \n", + " # Get responses from requested models\n", + " if 'gpt' in models:\n", + " console.print(Panel(f\"[bold blue]Getting response from {self.gpt_model}...[/bold blue]\"))\n", + " gpt_response = self.get_gpt_response(question)\n", + " responses['gpt'] = gpt_response\n", + " self.history[-1]['responses']['gpt'] = gpt_response\n", + " \n", + " if 'llama' in models:\n", + " console.print(Panel(f\"[bold green]Getting response from {self.llama_model}...[/bold green]\"))\n", + " llama_response = self.get_llama_response(question)\n", + " responses['llama'] = llama_response\n", + " self.history[-1]['responses']['llama'] = llama_response\n", + " display(Markdown(f\"## {self.llama_model} Response\\n{llama_response}\"))\n", + " \n", + " return responses\n", + " \n", + " def compare_responses(self, question: str = None) -> None:\n", + " \"\"\"\n", + " Compare responses from different models side by side.\n", + " \n", + " Args:\n", + " question: Optional specific question to compare responses for\n", + " \"\"\"\n", + " if question:\n", + " responses = self.ask(question)\n", + " else:\n", + " # Use the most recent question from history\n", + " if not self.history:\n", + " console.print(\"[bold red]No questions in history to compare[/bold red]\")\n", + " return\n", + " responses = self.history[-1]['responses']\n", + " question = self.history[-1]['question']\n", + " \n", + " # Create HTML for side-by-side comparison\n", + " html = f\"\"\"\n", + "
LLM Tutor initialized successfully!\n",
+ "\n"
+ ],
+ "text/plain": [
+ "\u001b[1;32mLLM Tutor initialized successfully!\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "\n",
+ "# Create a tutor instance\n",
+ "tutor = LLMTutor()\n",
+ "console.print(\"[bold green]LLM Tutor initialized successfully![/bold green]\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "25a36470-a68f-40f6-bea1-d2ebb173c015",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n", + "│ Question: │\n", + "│ │\n", + "│ Given a list of dictionaries called 'books', write code to find and print all information │\n", + "│ about the book titled 'Mastery' by Robert Greene. │\n", + "│ │\n", + "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n", + "\n" + ], + "text/plain": [ + "\u001b[34m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1mQuestion:\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Given a list of dictionaries called 'books', write code to find and print all information \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m about the book titled 'Mastery' by Robert Greene. \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "# Define your question here\n", + "question = \"\"\"\n", + "Given a list of dictionaries called 'books', write code to find and print all information \n", + "about the book titled 'Mastery' by Robert Greene.\n", + "\"\"\"\n", + "\n", + "console.print(Panel(f\"[bold]Question:[/bold]\\n{question}\", border_style=\"blue\"))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "bceaeaf9-4d08-4380-b757-597b851dd8ca", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ Getting response from gpt-4o-mini... │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+ "\n"
+ ],
+ "text/plain": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ \u001b[1;34mGetting response from gpt-4o-mini...\u001b[0m │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/markdown": [
+ "To find and print all information about the book titled \"Mastery\" by Robert Greene from a list of dictionaries called `books`, we can write a function that iterates through the list, checks for the specific title and author, and then prints the information if a match is found. Here's a step-by-step explanation followed by the code.\n",
+ "\n",
+ "### Steps to Follow:\n",
+ "\n",
+ "1. **Structure of the Data**: \n",
+ " Each book in the `books` list is a dictionary. We need to understand how the book's information is structured. A typical dictionary might look like this:\n",
+ " \n",
+ " {\n",
+ " 'title': 'Mastery',\n",
+ " 'author': 'Robert Greene',\n",
+ " 'year': 2012,\n",
+ " 'genre': 'Non-fiction',\n",
+ " 'isbn': '978-0143124177'\n",
+ " }\n",
+ " \n",
+ "\n",
+ "2. **Iterate through the List**:\n",
+ " We will use a loop to go through each book in the `books` list. \n",
+ "\n",
+ "3. **Check for Conditions**:\n",
+ " For each book (dictionary), we need to check if the 'title' is 'Mastery' and the 'author' is 'Robert Greene'. \n",
+ "\n",
+ "4. **Print the Details**: \n",
+ " If we find a match, we will print all the details of that book.\n",
+ "\n",
+ "### Example Code\n",
+ "\n",
+ "Here’s a Python code snippet that accomplishes this:\n",
+ "\n",
+ "\n",
+ "# Sample list of dictionaries representing books\n",
+ "books = [\n",
+ " {'title': 'Mastery', 'author': 'Robert Greene', 'year': 2012, 'genre': 'Non-fiction', 'isbn': '978-0143124177'},\n",
+ " {'title': 'The 48 Laws of Power', 'author': 'Robert Greene', 'year': 1998, 'genre': 'Non-fiction', 'isbn': '978-0140280197'},\n",
+ " {'title': 'The Art of War', 'author': 'Sun Tzu', 'year': '5th century BC', 'genre': 'Philosophy', 'isbn': '978-1590302255'}\n",
+ "]\n",
+ "\n",
+ "# Function to find and print information about the book titled 'Mastery' by Robert Greene\n",
+ "def find_book(books):\n",
+ " for book in books:\n",
+ " # Check if the title and author match\n",
+ " if book.get('title') == 'Mastery' and book.get('author') == 'Robert Greene':\n",
+ " # Print the entire dictionary if a match is found\n",
+ " print(\"Found book:\")\n",
+ " for key, value in book.items():\n",
+ " print(f\"{key}: {value}\")\n",
+ " return # Exit the function after finding the book\n",
+ " print(\"Book not found.\") # Optional: Print if the book is not in the list\n",
+ "\n",
+ "# Call the function\n",
+ "find_book(books)\n",
+ "\n",
+ "\n",
+ "### Explanation of the Code:\n",
+ "\n",
+ "1. **Data Structure**: The `books` variable is initialized as a list containing dictionary elements, where each dictionary represents a book.\n",
+ "\n",
+ "2. **Function Definition**: The function `find_book(books)` takes the list of books as an argument.\n",
+ "\n",
+ "3. **Iteration**: The `for` loop iterates over each book in the `books` list.\n",
+ "\n",
+ "4. **Finding the Match**: It checks if the title and author of the current book (retrieved using the `get` method to avoid `KeyError`) match 'Mastery' and 'Robert Greene'.\n",
+ "\n",
+ "5. **Printing Details**: If a match is found, it prints out the key-value pairs from the dictionary in a formatted manner.\n",
+ "\n",
+ "6. **Exit after Finding**: The `return` statement ensures that the function exits as soon as the book is found.\n",
+ "\n",
+ "7. **Not Found Condition**: If no book matches the criteria, it prints \"Book not found.\"\n",
+ "\n",
+ "### Conclusion\n",
+ "This method is efficient for small to moderately sized lists of dictionaries. If you have a very large dataset, consider using more efficient search algorithms or data structures like dictionaries for faster lookups, but the above approach should work well for typical use cases."
+ ],
+ "text/plain": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ Getting response from llama3.2... │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+ "\n"
+ ],
+ "text/plain": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ \u001b[1;32mGetting response from llama3.2...\u001b[0m │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/markdown": [
+ "## llama3.2 Response\n",
+ "Here's an example of how you can achieve this using Python:\n",
+ "\n",
+ "**Problem Statement**\n",
+ "\n",
+ "Given a list of dictionaries called `books`, where each dictionary represents a book with its title, author, publication year, etc., write code to find and print all information about the book titled `'Mastery'` by Robert Greene.\n",
+ "\n",
+ "**Example Input Data**\n",
+ "```python\n",
+ "books = [\n",
+ " {'title': 'Mastery', 'author': 'Robert Greene', 'publication_year': 2012, 'genre': 'Self-Help'},\n",
+ " {'title': 'The 48 Laws of Power', 'author': 'Robert Greene', 'publication_year': 2007, 'genre': 'Non-Fiction'},\n",
+ " {'title': 'To Kill a Mockingbird', 'author': 'Harper Lee', 'publication_year': 1960, 'genre': 'Classic Fiction'},\n",
+ " {'title': 'Mastery', 'author': 'Robert Greene', 'publication_year': 2018, 'genre': 'Self-Help'} # duplicate title\n",
+ "]\n",
+ "```\n",
+ "**Solution**\n",
+ "\n",
+ "Here's the Python code that finds and prints all information about the book titled `'Mastery'` by Robert Greene:\n",
+ "```python\n",
+ "# Define a function to find books with a specific title and author\n",
+ "def find_book(books, title, author):\n",
+ " \"\"\"\n",
+ " Find all books in the list that match the given title and author.\n",
+ "\n",
+ " Args:\n",
+ " books (list): List of dictionaries representing books.\n",
+ " title (str): Title of the book to search for.\n",
+ " author (str): Author of the book to search for.\n",
+ "\n",
+ " Returns:\n",
+ " list: List of dictionaries representing the found books.\n",
+ " \"\"\"\n",
+ " return [book for book in books if book['title'] == title and book['author'] == author]\n",
+ "\n",
+ "# Define a function to print book information\n",
+ "def print_book_info(book):\n",
+ " \"\"\"\n",
+ " Print all information about a single book.\n",
+ "\n",
+ " Args:\n",
+ " book (dict): Dictionary representing the book.\n",
+ " \"\"\"\n",
+ " print(f\"Title: {book['title']}\")\n",
+ " print(f\"Author: {book['author']}\")\n",
+ " print(f\"Publication Year: {book['publication_year']}\")\n",
+ " print(f\"Genre: {book['genre']}\\n\")\n",
+ "\n",
+ "# Find and print information about the book titled 'Mastery' by Robert Greene\n",
+ "target_title = \"Mastery\"\n",
+ "target_author = \"Robert Greene\"\n",
+ "\n",
+ "found_books = find_book(books, target_title, target_author)\n",
+ "\n",
+ "if found_books:\n",
+ " for i, book in enumerate(found_books):\n",
+ " print(f\"Book {i+1}:\")\n",
+ " print_book_info(book)\n",
+ "else:\n",
+ " print(f\"No books found with title '{target_title}' by author '{target_author}'.\")\n",
+ "```\n",
+ "**Explanation**\n",
+ "\n",
+ "The solution consists of two functions:\n",
+ "\n",
+ "1. `find_book`: This function takes a list of dictionaries representing books, as well as the title and author to search for. It uses a list comprehension to find all books that match the given criteria and returns them.\n",
+ "2. `print_book_info`: This function takes a single dictionary representing a book and prints its information.\n",
+ "\n",
+ "In the example code, we define the `books` list with some sample data. We then call the `find_book` function to find all books with the title `'Mastery'` by Robert Greene. If found books are returned, we iterate over them and print their information using the `print_book_info` function.\n",
+ "\n",
+ "Note that if there are duplicate titles in the input data, only one book will be returned by the `find_book` function, as dictionaries cannot have duplicate keys."
+ ],
+ "text/plain": [
+ "\n",
+ "Performance Statistics:\n",
+ "\n"
+ ],
+ "text/plain": [
+ "\n",
+ "\u001b[1mPerformance Statistics:\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "mean min max count\n", + "Model \n", + "gpt 14.672200 14.672200 14.672200 1\n", + "llama 79.891858 79.891858 79.891858 1\n", + "\n" + ], + "text/plain": [ + " mean min max count\n", + "Model \n", + "gpt \u001b[1;36m14.672200\u001b[0m \u001b[1;36m14.672200\u001b[0m \u001b[1;36m14.672200\u001b[0m \u001b[1;36m1\u001b[0m\n", + "llama \u001b[1;36m79.891858\u001b[0m \u001b[1;36m79.891858\u001b[0m \u001b[1;36m79.891858\u001b[0m \u001b[1;36m1\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAI/CAYAAABqNbq7AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQAAY3dJREFUeJzt3XtYFHX///HXqrhyVjywUKiIqJma5wNWagbmqbzN7rvsIFndlpoRFUre1WoGZqlklnYwxcos89DhLoPKKFPvUDO9yUxvj5lIeQJFEXV+f/hjvq6ggTIuK8/Hde11OZ/PZ2bes+sy+9qZnbEZhmEIAAAAAACUuyruLgAAAAAAgMsVoRsAAAAAAIsQugEAAAAAsAihGwAAAAAAixC6AQAAAACwCKEbAAAAAACLELoBAAAAALAIoRsAAAAAAIsQugEAAAAAsAihGxdlzpw5stls5qNatWoKCQnR7bffrs2bN7u7PI8TGxvr8nye6xEbG6tvvvlGNptN33zzjbvLNnXv3t2ssUqVKvL391fjxo1122236cMPP9SpU6eKzdOwYUPFxsaWaT0rVqyQ0+nUwYMHyzTf2esqeg4//PDDMi3nfPLz8+V0Okt8XYreL9u3by+39QFARTRt2jTZbDa1aNHC3aVUOGfuK202m2rUqKHmzZtrwoQJOn78uLvL8zil+dxU9HkpNjZWDRs2dHfJqISqubsAXB5mz56tZs2a6dixY/r+++/13HPPadmyZfrll19Uq1Ytd5fnMZ566ik9+OCD5vTatWs1YsQIJSUlqUePHmZ73bp1VbduXa1cuVLNmzd3R6nn1KhRI7377ruSpCNHjmjbtm1asmSJbrvtNl133XX65JNPFBgYaI5fvHixAgICyrSOFStWaNy4cYqNjVXNmjVLPd+FrKus8vPzNW7cOEmnP1idqW/fvlq5cqVCQkIsrQEA3O2tt96SJGVlZek///mPOnXq5OaKKpYz95V//PGH3nzzTT311FPauXOnXn/9dTdX51lWrlzpMv3ss89q2bJl+vrrr13amzdvrrCwMD3yyCOXsjxAEqEb5aRFixZq3769pNNB4+TJk3rmmWe0ZMkS3XvvvW6uznNEREQoIiLCnD527JgkKTIyUp07dy42vqQ2d/P29i5W1/3336/Zs2dr6NCh+uc//6n333/f7GvTpo3lNR09elTe3t6XZF3nU/RlCQBczlavXq2ffvpJffv21b///W/NmjXrkoduwzB07NgxeXt7X9L1ltbZ+8revXurefPmSk1N1bRp01SjRg03VudZzv7MUbduXVWpUqXEz0hWf/EOnAunl8MSRQF87969Lu2rV6/WzTffrKCgINWoUUNt2rTRBx984DImPz9fjz/+uMLDw1WjRg0FBQWpffv2eu+998wxsbGx8vPzU1ZWlnr27ClfX1/VrVtXI0eOVH5+vsvyjh07psTERIWHh6t69eq64oorNGLEiGKnJjds2FD9+vXT0qVL1bZtW3l7e6tZs2bmt/Vlqa+023oxSjq9vOh5+eWXX9SrVy/5+voqJCREEydOlCStWrVK1157rXx9fdWkSROlpqYWW252draGDRumK6+8UtWrV1d4eLjGjRunEydOXFS99957r/r06aMFCxZox44dZvvZp3yfOnVKEyZMUNOmTeXt7a2aNWuqVatWeumllyRJTqdTTzzxhCQpPDzc5bSxouX169dPixYtUps2bVSjRg3zyPO5TmU/duyY4uPj5XA45O3trW7duunHH390GdO9e/diR64luZyqtn37djNUjxs3zuXnANK5Ty9/6623dM0115j/n/72t79p48aNxdbj5+enLVu2qE+fPvLz81NYWJgee+wxFRQUnPN5B4BLbdasWZKkiRMnKioqSvPnzzf3zYWFhapXr57uvvvuYvMdPHhQ3t7eio+PN9tyc3PNfW7RPjwuLk5Hjhxxmddms2nkyJGaOXOmrrrqKtntdnMfN27cOHXq1ElBQUEKCAhQ27ZtNWvWLBmG4bKMgoICPfbYY3I4HPLx8dH111+vNWvWlLjvKO99ZbVq1dS6dWsdP37c5fOJYRh69dVX1bp1a3l7e6tWrVoaNGiQtm7d6jL/jz/+qH79+qlevXqy2+0KDQ1V37599dtvvxV7jl577TU1adJEdrtdzZs31/z584vV89///le33HKLatWqpRo1aqh169bFPjMUfQ557733NHbsWIWGhiogIEA33nijNm3aVOb6SrutF6Ok08uLnpfZs2ebnz3at2+vVatWyTAMvfDCCwoPD5efn59uuOEGbdmypdhyv/zyS/Xs2VMBAQHy8fFR165d9dVXX5Vb3fB8HOmGJbZt2yZJatKkidm2bNky3XTTTerUqZNmzpypwMBAzZ8/X//4xz+Un59v7tDi4+P19ttva8KECWrTpo2OHDmi//73v9q3b5/LOgoLC9WnTx8NGzZMY8aM0YoVKzRhwgTt2LFDn3zyiaTTf8AHDBigr776SomJibruuuu0fv16PfPMM1q5cqVWrlwpu91uLvOnn37SY489pjFjxig4OFhvvvmm7rvvPjVu3FjXX399qesr7bZaobCwUAMHDtSDDz6oJ554QvPmzVNiYqJyc3O1cOFCjR49WldeeaVefvllxcbGqkWLFmrXrp2k0x8iOnbsqCpVqujpp59WRESEVq5cqQkTJmj79u2aPXv2RdV2880367PPPtN3332nBg0alDhm0qRJcjqd+te//qXrr79ehYWF+uWXX8wPIffff7/279+vl19+WYsWLTJP1T7zNPu1a9dq48aN+te//qXw8HD5+vqet64nn3xSbdu21ZtvvqlDhw7J6XSqe/fu+vHHH9WoUaNSb19ISIiWLl2qm266Sffdd5/uv/9+STrv0e3k5GQ9+eSTuuOOO5ScnKx9+/bJ6XSqS5cuyszMVGRkpDm2sLBQN998s+677z499thj+vbbb/Xss88qMDBQTz/9dKnrBACrHD16VO+99546dOigFi1aaOjQobr//vu1YMECDRkyRF5eXrrrrrs0c+ZMvfLKKy5HHt977z0dO3bMPEMuPz9f3bp102+//aYnn3xSrVq1UlZWlp5++mlt2LBBX375pWw2mzn/kiVL9N133+npp5+Ww+FQvXr1JJ3+QnTYsGGqX7++pNNfQD/88MPavXu3y9/Oe++9V++//74SEhJ0ww036Oeff9bf/vY35ebmumyjVfvKbdu2qWbNmi77jGHDhmnOnDkaNWqUnn/+ee3fv1/jx49XVFSUfvrpJwUHB+vIkSOKjo5WeHi4XnnlFQUHBys7O1vLli1TXl6eyzo+/vhjLVu2TOPHj5evr69effVV3XHHHapWrZoGDRokSdq0aZOioqJUr149TZs2TbVr19Y777yj2NhY7d27VwkJCS7LfPLJJ9W1a1e9+eabys3N1ejRo9W/f39t3LhRVatWLXV9pdlWq3z66af68ccfNXHiRNlsNo0ePVp9+/bVkCFDtHXrVk2fPl2HDh1SfHy8br31Vq1bt878v/fOO+/onnvu0S233KLU1FR5eXnptddeU69evfTFF1+oZ8+eltUND2IAF2H27NmGJGPV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f/XVV9q7d68ZBE+ePKn3339fERERuvLKKyWd/uy0ePFi/f777y5fIMydO1c+Pj4XdbvSc9V3IdtaEXTt2lU1a9bUzz//rJEjR7q7HFRghG5YolatWkpMTFRCQoLmzZunu+66S6+99pp69+6tXr16KTY2VldccYX279+vjRs3au3atVqwYIEkqVOnTurXr59atWqlWrVqaePGjXr77bfVpUsXl8BdvXp1TZ48WYcPH1aHDh3Mq5f37t1b1157rSQpOjpavXr10ujRo5Wbm6uuXbuaVy9v06ZNibcs+Sulqa+021rRjB8/Xunp6YqKitKoUaPUtGlTHTt2TNu3b9dnn32mmTNnmjvlczl69KhWrVpl/nvr1q1asmSJPv30U3Xr1k0zZ8487/z9+/c37/tet25d7dixQykpKWrQoIF5Je+WLVtKkl566SXzarhNmzaVv7//BW13Tk6O/va3v+mBBx7QoUOH9Mwzz6hGjRpKTEw0x9x999167bXXdNddd+mBBx7Qvn37NGnSpGL3/PT391eDBg300UcfqWfPngoKClKdOnWK3aJEOv2lwlNPPaUnn3xS99xzj+644w7t27dP48aNU40aNfTMM89c0PYAwKX2+eef6/fff9fzzz9f4u0VW7RooenTp2vWrFnq16+fpNOnmL///vsaOXKkrrzySt14440u88TFxWnhwoW6/vrr9eijj6pVq1Y6deqUdu7cqbS0ND322GN/ef/vvn37asqUKRo8eLD++c9/at++fXrxxReLHTm9+uqrdccdd2jy5MmqWrWqbrjhBmVlZWny5MkKDAxUlSr/94vM8thXluSee+7RlClT9OKLL2rEiBHq2rWr/vnPf+ree+/V6tWrdf3118vX11d79uzR8uXL1bJlSz300EP69NNP9eqrr2rAgAFq1KiRDMPQokWLdPDgQUVHR7uso06dOrrhhhv01FNPmVcv/+WXX1xOa3/mmWfM360//fTTCgoK0rvvvqt///vfmjRpkgIDA8u0XaWpr7TbWtH4+fnp5Zdf1pAhQ7R//34NGjRI9erV0x9//KGffvpJf/zxh2bMmOHuMlERuOf6bbhcFF2N+ewrixrG6St/169f34iMjDROnDhhGIZh/PTTT8bf//53o169eoaXl5fhcDiMG264weVqnWPGjDHat29v1KpVy7Db7UajRo2MRx991Pjz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1pednS1JCg4OdmkPDg42+0pSUFCg3NxclwcAAJUVVy8HAMADPfXUU7r11lv1v//9TzfccIMk6auvvtJ7772nBQsWlGoZu3bt0iOPPKK0tDTVqFHjnONsNpvLtGEYxdrOlJycrHHjxpWqBgAALncc6QYAwAPdfPPNWrJkibZs2aLhw4frscce02+//aYvv/xSAwYMKNUy1qxZo5ycHLVr107VqlVTtWrVlJGRoWnTpqlatWrmEe6zj2rn5OQUO/p9psTERB06dMh87Nq164K3EwAAT8eRbgAAPFTfvn1LvJhaafXs2VMbNmxwabv33nvVrFkzjR49Wo0aNZLD4VB6erratGkjSTp+/LgyMjL0/PPPn3O5drtddrv9gusCAOByQugGAMBDHTx4UB9++KG2bt2qxx9/XEFBQVq7dq2Cg4N1xRVX/OX8/v7+atGihUubr6+vateubbbHxcUpKSlJkZGRioyMVFJSknx8fDR48GBLtgkAgMsNoRsAAA+0fv163XjjjQoMDNT27dt1//33KygoSIsXL9aOHTs0d+7ccllPQkKCjh49quHDh+vAgQPq1KmT0tLS5O/vXy7LBwDgckfoBgDAA8XHxys2NlaTJk1yCcC9e/e+qKPQ33zzjcu0zWaT0+mU0+m84GUCAFCZcSE1AAA8UGZmpoYNG1as/Yorrjjv7bwAAMClRegGAMAD1ahRo8T7X2/atEl169Z1Q0UAAKAkhG4AADzQLbfcovHjx6uwsFDS6dPAd+7cqTFjxujWW291c3UAAKAIoRsAAA/04osv6o8//lC9evV09OhRdevWTREREfLz89Nzzz3n7vIAAMD/x4XUAADwQAEBAVq+fLm+/vprrV27VqdOnVK7du3Us2dPd5cGAADOwJFuAAA8yH/+8x99/vnn5vQNN9ygunXr6tVXX9Udd9yhf/7znyooKHBjhQAA4EyEbgAAPIjT6dT69evN6Q0bNuiBBx5QdHS0xowZo08++UTJyclurBAAAJyJ0A0AgAdZt26dyynk8+fPV8eOHfXGG28oPj5e06ZN0wcffODGCgEAwJkI3QAAeJADBw4oODjYnM7IyNBNN91kTnfo0EG7du1yR2kAAKAEhG4AADxIcHCwtm3bJkk6fvy41q5dqy5dupj9eXl58vLycld5AADgLIRuAAA8yE033aQxY8bou+++U2Jionx8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", + "text/plain": [ + "
History saved to my_tutor_session.json\n",
+ "\n"
+ ],
+ "text/plain": [
+ "\u001b[1;32mHistory saved to my_tutor_session.json\u001b[0m\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "\n",
+ "# Save history to a file\n",
+ "tutor.save_history(\"my_tutor_session.json\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "4aa6afbf-1cc1-4ed1-a65f-14ee02ce278f",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n", + "│ New Question: │\n", + "│ Explain how to implement a binary search algorithm in Python. │\n", + "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n", + "\n" + ], + "text/plain": [ + "\u001b[32m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m\n", + "\u001b[32m│\u001b[0m \u001b[1mNew Question:\u001b[0m \u001b[32m│\u001b[0m\n", + "\u001b[32m│\u001b[0m Explain how to implement a binary search algorithm in Python. \u001b[32m│\u001b[0m\n", + "\u001b[32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ Getting response from gpt-4o-mini... │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+ "\n"
+ ],
+ "text/plain": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ \u001b[1;34mGetting response from gpt-4o-mini...\u001b[0m │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/markdown": [
+ "Binary search is an efficient algorithm for finding a target value within a sorted list. It works by repeatedly dividing the search interval in half. If the target value is less than the element in the middle of the interval, the search continues on the lower half; otherwise, it continues on the upper half. This process is continued until the target value is found or the search interval is empty.\n",
+ "\n",
+ "Here's a detailed explanation and implementation of the binary search algorithm in Python:\n",
+ "\n",
+ "### Step-by-Step Implementation\n",
+ "\n",
+ "1. **Prerequisites**:\n",
+ " - Ensure the input list is sorted. Binary search can only be performed on a sorted list.\n",
+ " \n",
+ "2. **Set Initial Variables**:\n",
+ " - Define two pointers, `low` and `high`, which represent the starting and ending indices of the search range in the list.\n",
+ "\n",
+ "3. **Calculate the Middle Index**:\n",
+ " - Use the formula `mid = (low + high) // 2` to find the middle index.\n",
+ "\n",
+ "4. **Comparison**:\n",
+ " - Compare the middle element with the target:\n",
+ " - If the middle element is equal to the target, return the index of the middle element.\n",
+ " - If the target is less than the middle element, narrow the search to the left half by setting `high = mid - 1`.\n",
+ " - If the target is greater than the middle element, narrow the search to the right half by setting `low = mid + 1`.\n",
+ "\n",
+ "5. **Loop Until the Target is Found or the Interval is Empty**:\n",
+ " - Repeat the above steps until the `low` pointer exceeds the `high` pointer. If the target is not found, return a value indicating that the target is not present (commonly -1).\n",
+ "\n",
+ "### Implementation in Python\n",
+ "\n",
+ "Here’s a complete Python implementation of the binary search algorithm:\n",
+ "\n",
+ "\n",
+ "def binary_search(arr, target):\n",
+ " low = 0\n",
+ " high = len(arr) - 1\n",
+ "\n",
+ " while low <= high:\n",
+ " # Find the middle index\n",
+ " mid = (low + high) // 2\n",
+ " \n",
+ " # Check if the target is present at mid\n",
+ " if arr[mid] == target:\n",
+ " return mid # Target found, return the index\n",
+ " \n",
+ " # If the target is smaller than the mid element,\n",
+ " # it can only be present in the left subarray\n",
+ " elif arr[mid] > target:\n",
+ " high = mid - 1\n",
+ " \n",
+ " # If the target is larger than the mid element,\n",
+ " # it can only be present in the right subarray\n",
+ " else:\n",
+ " low = mid + 1\n",
+ "\n",
+ " # Target was not found\n",
+ " return -1\n",
+ "\n",
+ "\n",
+ "### Example Usage\n",
+ "\n",
+ "\n",
+ "# Example sorted list\n",
+ "arr = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]\n",
+ "target = 7\n",
+ "\n",
+ "# Perform binary search\n",
+ "result = binary_search(arr, target)\n",
+ "\n",
+ "if result != -1:\n",
+ " print(f'Target {target} found at index {result}.')\n",
+ "else:\n",
+ " print(f'Target {target} not found in the list.')\n",
+ "\n",
+ "\n",
+ "### Key Points\n",
+ "\n",
+ "1. **Time Complexity**: The time complexity of binary search is O(log n), where n is the number of elements in the array. This is significantly more efficient than a linear search, which has a time complexity of O(n).\n",
+ " \n",
+ "2. **Space Complexity**: The space complexity of the binary search algorithm is O(1) for the iterative version, as it requires a fixed amount of space for variables.\n",
+ "\n",
+ "3. **Iterative vs Recursive**: The above implementation is iterative, which is generally preferred for binary search due to its efficiency and avoidance of recursion limits. However, a recursive implementation can also be done:\n",
+ "\n",
+ "### Recursive Implementation\n",
+ "\n",
+ "\n",
+ "def binary_search_recursive(arr, target, low, high):\n",
+ " if low <= high:\n",
+ " mid = (low + high) // 2\n",
+ " \n",
+ " if arr[mid] == target:\n",
+ " return mid\n",
+ " elif arr[mid] > target:\n",
+ " return binary_search_recursive(arr, target, low, mid - 1)\n",
+ " else:\n",
+ " return binary_search_recursive(arr, target, mid + 1, high)\n",
+ " \n",
+ " return -1\n",
+ "\n",
+ "\n",
+ "### Conclusion\n",
+ "\n",
+ "Binary search is a fundamental searching technique that exploits the properties of sorted arrays. Its efficiency makes it a preferred method for searching when working with large datasets. Understanding its underlying algorithm and being able to implement it in Python is a valuable skill in software engineering and data science."
+ ],
+ "text/plain": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ Getting response from llama3.2... │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+ "\n"
+ ],
+ "text/plain": [
+ "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
+ "│ \u001b[1;32mGetting response from llama3.2...\u001b[0m │\n",
+ "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/markdown": [
+ "## llama3.2 Response\n",
+ "**Implementing Binary Search Algorithm in Python**\n",
+ "=====================================================\n",
+ "\n",
+ "Binary search is an efficient algorithm for finding an item from a sorted list of items. It works by repeatedly dividing in half the portion of the list that could contain the item, until you've narrowed down the possible locations to just one.\n",
+ "\n",
+ "Here's a step-by-step guide on how to implement binary search in Python:\n",
+ "\n",
+ "**Step 1: Define the Search Function**\n",
+ "-------------------------------------\n",
+ "\n",
+ "```python\n",
+ "def binary_search(arr, target):\n",
+ " \"\"\"\n",
+ " Searches for an element in a sorted array using binary search algorithm.\n",
+ " \n",
+ " Parameters:\n",
+ " arr (list): The sorted list of elements.\n",
+ " target: The element to be searched.\n",
+ " \n",
+ " Returns:\n",
+ " int: The index of the target element if found; otherwise, -1.\n",
+ " \"\"\"\n",
+ "```\n",
+ "\n",
+ "**Step 2: Initialize Variables**\n",
+ "---------------------------------\n",
+ "\n",
+ "```python\n",
+ " low = 0 # Index of the first element in the list\n",
+ " high = len(arr) - 1 # Index of the last element in the list\n",
+ "```\n",
+ "\n",
+ "**Step 3: Loop Until Found or Not Found**\n",
+ "-----------------------------------------\n",
+ "\n",
+ "```python\n",
+ " while low <= high:\n",
+ " mid = (low + high) // 2 # Calculate the middle index\n",
+ " \n",
+ " if arr[mid] == target:\n",
+ " return mid # Target found, return its index\n",
+ " \n",
+ " elif arr[mid] < target:\n",
+ " low = mid + 1 # Search in the right half\n",
+ " \n",
+ " else:\n",
+ " high = mid - 1 # Search in the left half\n",
+ "```\n",
+ "\n",
+ "**Step 4: Handle Edge Cases**\n",
+ "---------------------------\n",
+ "\n",
+ "```python\n",
+ " if low > high:\n",
+ " return -1 # Target not found, return -1\n",
+ "```\n",
+ "\n",
+ "**Putting it all Together**\n",
+ "-----------------------------\n",
+ "\n",
+ "Here's the complete binary search implementation in Python:\n",
+ "\n",
+ "```python\n",
+ "def binary_search(arr, target):\n",
+ " \"\"\"\n",
+ " Searches for an element in a sorted array using binary search algorithm.\n",
+ " \n",
+ " Parameters:\n",
+ " arr (list): The sorted list of elements.\n",
+ " target: The element to be searched.\n",
+ " \n",
+ " Returns:\n",
+ " int: The index of the target element if found; otherwise, -1.\n",
+ " \"\"\"\n",
+ " low = 0\n",
+ " high = len(arr) - 1\n",
+ "\n",
+ " while low <= high:\n",
+ " mid = (low + high) // 2\n",
+ " \n",
+ " if arr[mid] == target:\n",
+ " return mid\n",
+ " elif arr[mid] < target:\n",
+ " low = mid + 1\n",
+ " else:\n",
+ " high = mid - 1\n",
+ " \n",
+ " return -1\n",
+ "\n",
+ "# Example usage\n",
+ "arr = [2, 4, 6, 8, 10]\n",
+ "target = 6\n",
+ "index = binary_search(arr, target)\n",
+ "if index != -1:\n",
+ " print(f\"Target {target} found at index {index}\")\n",
+ "else:\n",
+ " print(\"Target not found\")\n",
+ "```\n",
+ "\n",
+ "**Time Complexity**\n",
+ "------------------\n",
+ "\n",
+ "The time complexity of binary search is O(log n), where n is the length of the input array. This makes it much faster than linear search (O(n)) for large datasets.\n",
+ "\n",
+ "I hope this explanation helps! Let me know if you have any further questions or need additional clarification."
+ ],
+ "text/plain": [
+ "