diff --git a/community-contributions/abdoul/week_eight_exercise.ipynb b/community-contributions/abdoul/week_eight_exercise.ipynb
new file mode 100644
index 0000000..c08bce7
--- /dev/null
+++ b/community-contributions/abdoul/week_eight_exercise.ipynb
@@ -0,0 +1,487 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Week 8: Multi-Agent Deal Hunting System\n",
+ "\n",
+ "This notebook demonstrates Week 8 concepts:\n",
+ "- Multi-agent architecture with specialized agents\n",
+ "- Real-time Gradio UI with threading and queues\n",
+ "- Integration with deployed Modal services\n",
+ "- RAG pipeline with ChromaDB\n",
+ "- Ensemble model combining multiple AI approaches"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "!pip install -q gradio pydantic openai chromadb sentence-transformers scikit-learn feedparser beautifulsoup4 requests plotly"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import logging\n",
+ "import queue\n",
+ "import threading\n",
+ "import time\n",
+ "import json\n",
+ "from typing import List, Optional\n",
+ "from datetime import datetime\n",
+ "\n",
+ "import gradio as gr\n",
+ "import plotly.graph_objects as go\n",
+ "from pydantic import BaseModel"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class Deal(BaseModel):\n",
+ " product_description: str\n",
+ " price: float\n",
+ " url: str\n",
+ "\n",
+ "\n",
+ "class DealSelection(BaseModel):\n",
+ " deals: List[Deal]\n",
+ "\n",
+ "\n",
+ "class Opportunity(BaseModel):\n",
+ " deal: Deal\n",
+ " estimate: float\n",
+ " discount: float\n",
+ "\n",
+ "\n",
+ "class MockAgent:\n",
+ " name = \"Mock Agent\"\n",
+ " color = '\\033[37m'\n",
+ " \n",
+ " def log(self, message):\n",
+ " logging.info(f\"[{self.name}] {message}\")\n",
+ "\n",
+ "\n",
+ "class MockScannerAgent(MockAgent):\n",
+ " name = \"Scanner Agent\"\n",
+ " \n",
+ " def scan(self, memory=None):\n",
+ " self.log(\"Simulating RSS feed scan\")\n",
+ " time.sleep(1)\n",
+ " \n",
+ " deals = [\n",
+ " Deal(\n",
+ " product_description=\"Apple iPad Pro 11-inch 256GB WiFi (latest model) - Space Gray. Features M2 chip, Liquid Retina display, 12MP camera, Face ID, and all-day battery life.\",\n",
+ " price=749.99,\n",
+ " url=\"https://example.com/ipad\"\n",
+ " ),\n",
+ " Deal(\n",
+ " product_description=\"Sony WH-1000XM5 Wireless Noise Cancelling Headphones - Industry-leading noise cancellation, exceptional sound quality, 30-hour battery life, comfortable design.\",\n",
+ " price=329.99,\n",
+ " url=\"https://example.com/sony-headphones\"\n",
+ " )\n",
+ " ]\n",
+ " \n",
+ " return DealSelection(deals=deals)\n",
+ "\n",
+ "\n",
+ "class MockEnsembleAgent(MockAgent):\n",
+ " name = \"Ensemble Agent\"\n",
+ " \n",
+ " def price(self, description: str) -> float:\n",
+ " self.log(f\"Estimating price for product\")\n",
+ " time.sleep(0.5)\n",
+ " \n",
+ " if \"iPad\" in description:\n",
+ " return 899.00\n",
+ " elif \"Sony\" in description:\n",
+ " return 398.00\n",
+ " else:\n",
+ " return 150.00\n",
+ "\n",
+ "\n",
+ "class MockMessagingAgent(MockAgent):\n",
+ " name = \"Messaging Agent\"\n",
+ " \n",
+ " def alert(self, opportunity: Opportunity):\n",
+ " self.log(f\"Alert sent: ${opportunity.discount:.2f} discount on {opportunity.deal.product_description[:50]}...\")\n",
+ "\n",
+ "\n",
+ "class MockPlanningAgent(MockAgent):\n",
+ " name = \"Planning Agent\"\n",
+ " DEAL_THRESHOLD = 50\n",
+ " \n",
+ " def __init__(self):\n",
+ " self.scanner = MockScannerAgent()\n",
+ " self.ensemble = MockEnsembleAgent()\n",
+ " self.messenger = MockMessagingAgent()\n",
+ " \n",
+ " def plan(self, memory=None) -> Optional[Opportunity]:\n",
+ " if memory is None:\n",
+ " memory = []\n",
+ " \n",
+ " self.log(\"Starting planning cycle\")\n",
+ " \n",
+ " selection = self.scanner.scan(memory)\n",
+ " \n",
+ " if selection and selection.deals:\n",
+ " opportunities = []\n",
+ " for deal in selection.deals:\n",
+ " estimate = self.ensemble.price(deal.product_description)\n",
+ " discount = estimate - deal.price\n",
+ " opportunities.append(Opportunity(\n",
+ " deal=deal,\n",
+ " estimate=estimate,\n",
+ " discount=discount\n",
+ " ))\n",
+ " \n",
+ " opportunities.sort(key=lambda x: x.discount, reverse=True)\n",
+ " best = opportunities[0]\n",
+ " \n",
+ " self.log(f\"Best deal has discount: ${best.discount:.2f}\")\n",
+ " \n",
+ " if best.discount > self.DEAL_THRESHOLD:\n",
+ " self.messenger.alert(best)\n",
+ " return best\n",
+ " \n",
+ " return None\n",
+ "\n",
+ "\n",
+ "class MockDealAgentFramework:\n",
+ " MEMORY_FILE = \"mock_memory.json\"\n",
+ " \n",
+ " def __init__(self):\n",
+ " self.memory = self.read_memory()\n",
+ " self.planner = None\n",
+ " \n",
+ " def init_agents_as_needed(self):\n",
+ " if not self.planner:\n",
+ " logging.info(\"Initializing Mock Agent Framework\")\n",
+ " self.planner = MockPlanningAgent()\n",
+ " logging.info(\"Mock Agent Framework ready\")\n",
+ " \n",
+ " def read_memory(self) -> List[Opportunity]:\n",
+ " if os.path.exists(self.MEMORY_FILE):\n",
+ " try:\n",
+ " with open(self.MEMORY_FILE, 'r') as f:\n",
+ " data = json.load(f)\n",
+ " return [Opportunity(**item) for item in data]\n",
+ " except:\n",
+ " return []\n",
+ " return []\n",
+ " \n",
+ " def write_memory(self):\n",
+ " data = [opp.dict() for opp in self.memory]\n",
+ " with open(self.MEMORY_FILE, 'w') as f:\n",
+ " json.dump(data, f, indent=2)\n",
+ " \n",
+ " def run(self) -> List[Opportunity]:\n",
+ " self.init_agents_as_needed()\n",
+ " result = self.planner.plan(memory=self.memory)\n",
+ " \n",
+ " if result:\n",
+ " self.memory.append(result)\n",
+ " self.write_memory()\n",
+ " \n",
+ " return self.memory\n",
+ " \n",
+ " @classmethod\n",
+ " def get_plot_data(cls, max_datapoints=100):\n",
+ " import numpy as np\n",
+ " \n",
+ " n_points = min(100, max_datapoints)\n",
+ " vectors = np.random.randn(n_points, 3)\n",
+ " documents = [f\"Product {i}\" for i in range(n_points)]\n",
+ " colors = ['red', 'blue', 'green', 'orange'] * (n_points // 4 + 1)\n",
+ " \n",
+ " return documents[:n_points], vectors, colors[:n_points]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "BG_BLACK = '\\033[40m'\n",
+ "RED = '\\033[31m'\n",
+ "GREEN = '\\033[32m'\n",
+ "YELLOW = '\\033[33m'\n",
+ "BLUE = '\\033[34m'\n",
+ "MAGENTA = '\\033[35m'\n",
+ "CYAN = '\\033[36m'\n",
+ "WHITE = '\\033[37m'\n",
+ "BG_BLUE = '\\033[44m'\n",
+ "RESET = '\\033[0m'\n",
+ "\n",
+ "color_mapper = {\n",
+ " BG_BLACK+RED: \"#dd0000\",\n",
+ " BG_BLACK+GREEN: \"#00dd00\",\n",
+ " BG_BLACK+YELLOW: \"#dddd00\",\n",
+ " BG_BLACK+BLUE: \"#0000ee\",\n",
+ " BG_BLACK+MAGENTA: \"#aa00dd\",\n",
+ " BG_BLACK+CYAN: \"#00dddd\",\n",
+ " BG_BLACK+WHITE: \"#87CEEB\",\n",
+ " BG_BLUE+WHITE: \"#ff7800\"\n",
+ "}\n",
+ "\n",
+ "\n",
+ "def reformat_log(message):\n",
+ " for key, value in color_mapper.items():\n",
+ " message = message.replace(key, f'')\n",
+ " message = message.replace(RESET, '')\n",
+ " return message\n",
+ "\n",
+ "\n",
+ "class QueueHandler(logging.Handler):\n",
+ " def __init__(self, log_queue):\n",
+ " super().__init__()\n",
+ " self.log_queue = log_queue\n",
+ "\n",
+ " def emit(self, record):\n",
+ " self.log_queue.put(self.format(record))\n",
+ "\n",
+ "\n",
+ "def setup_logging(log_queue):\n",
+ " handler = QueueHandler(log_queue)\n",
+ " formatter = logging.Formatter(\n",
+ " \"[%(asctime)s] %(message)s\",\n",
+ " datefmt=\"%Y-%m-%d %H:%M:%S\",\n",
+ " )\n",
+ " handler.setFormatter(formatter)\n",
+ " logger = logging.getLogger()\n",
+ " logger.addHandler(handler)\n",
+ " logger.setLevel(logging.INFO)\n",
+ "\n",
+ "\n",
+ "def html_for(log_data):\n",
+ " output = '
'.join(log_data[-20:])\n",
+ " return f\"\"\"\n",
+ "