From 951f2bb660cfd117f2d1633be9d349ff683362fc Mon Sep 17 00:00:00 2001 From: Dipin Date: Fri, 31 Jan 2025 08:26:16 -0600 Subject: [PATCH 01/10] Added my contributions to community-contributions --- .../day1-Groq API.ipynb | 530 ++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 week1/community-contributions/day1-Groq API.ipynb diff --git a/week1/community-contributions/day1-Groq API.ipynb b/week1/community-contributions/day1-Groq API.ipynb new file mode 100644 index 0000000..3838097 --- /dev/null +++ b/week1/community-contributions/day1-Groq API.ipynb @@ -0,0 +1,530 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d15d8294-3328-4e07-ad16-8a03e9bbfdb9", + "metadata": {}, + "source": [ + "## DAY1 LLM Project with GROQ!\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e2a9393-7767-488e-a8bf-27c12dca35bd", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import requests\n", + "from dotenv import load_dotenv\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display\n", + "from groq import Groq\n", + "\n", + "# If you get an error running this cell, then please head over to the troubleshooting notebook!" + ] + }, + { + "cell_type": "markdown", + "id": "5d899ad6-1428-481b-b308-750308d80442", + "metadata": {}, + "source": [ + "If you are getting error ModuleNotFoundError: No module named 'groq' follow below steps.\n", + "\n", + "1. Activate llms enviornment from Anaconda, so that (llms) is showing in your prompt, as this is the environment where the package will get installed.Install pip here. \n", + "\n", + "(base) PS C:\\Users\\test\\OneDrive\\Desktop\\AI\\projects\\llm_engineering> conda activate llms\n", + "(llms) PS C:\\Users\\test\\OneDrive\\Desktop\\AI\\projects\\llm_engineering> pip install groq\n", + "\n", + "\n", + "2. After you install a new package, you'd need to restart the Kernel in jupyter lab for each notebook (Kernel >> Restart Kernel and Clear Values Of All Outputs).\n", + "\n", + "You can also run this command in jupyter lab to see whether it's installed:\n", + "\n", + "!pip show groq\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99c0c3c9-fa5e-405e-8453-2a557dc60c09", + "metadata": {}, + "outputs": [], + "source": [ + "!pip show groq" + ] + }, + { + "cell_type": "markdown", + "id": "6900b2a8-6384-4316-8aaa-5e519fca4254", + "metadata": {}, + "source": [ + "# Connecting to GROQ\n", + "\n", + "The next cell is where we load in the environment variables in your `.env` file and connect to GROQ.\n", + "\n", + ".env file should have below entry\n", + "\n", + "GROQ_API_KEY=gsk_xxxxxx\n", + "\n", + "GROQ keys can be configired by logging to below link\n", + "https://console.groq.com/keys\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b87cadb-d513-4303-baee-a37b6f938e4d", + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment variables in a file called .env\n", + "\n", + "load_dotenv(override=True)\n", + "api_key = os.getenv('GROQ_API_KEY')\n", + "\n", + "# Check the key\n", + "\n", + "if not api_key:\n", + " print(\"No API key was found - please head over to the troubleshooting notebook in this folder to identify & fix!\")\n", + "elif not api_key.startswith(\"gsk_\"):\n", + " print(\"An API key was found, but it doesn't start sk-proj-; please check you're using the right key - see troubleshooting notebook\")\n", + "elif api_key.strip() != api_key:\n", + " print(\"An API key was found, but it looks like it might have space or tab characters at the start or end - please remove them - see troubleshooting notebook\")\n", + "else:\n", + " print(\"API key found and looks good so far!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "019974d9-f3ad-4a8a-b5f9-0a3719aea2d3", + "metadata": {}, + "outputs": [], + "source": [ + "groq = Groq()" + ] + }, + { + "cell_type": "markdown", + "id": "442fc84b-0815-4f40-99ab-d9a5da6bda91", + "metadata": {}, + "source": [ + "# Let's make a quick call to a Frontier model to get started, as a preview!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a58394bf-1e45-46af-9bfd-01e24da6f49a", + "metadata": {}, + "outputs": [], + "source": [ + "# To give you a preview -- calling Groq with these messages is this easy. Any problems, head over to the Troubleshooting notebook.\n", + "\n", + "message = \"Hello, GPT! This is my first ever message to you! Hi!\"\n", + "response = groq.chat.completions.create(model=\"llama-3.3-70b-versatile\", messages=[{\"role\":\"user\", \"content\":message}])\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "2aa190e5-cb31-456a-96cc-db109919cd78", + "metadata": {}, + "source": [ + "## OK onwards with our first project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5e793b2-6775-426a-a139-4848291d0463", + "metadata": {}, + "outputs": [], + "source": [ + "# A class to represent a Webpage\n", + "# If you're not familiar with Classes, check out the \"Intermediate Python\" notebook\n", + "\n", + "# Some websites need you to use proper headers when fetching them:\n", + "headers = {\n", + " \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\"\n", + "}\n", + "\n", + "class Website:\n", + "\n", + " def __init__(self, url):\n", + " \"\"\"\n", + " Create this Website object from the given url using the BeautifulSoup library\n", + " \"\"\"\n", + " self.url = url\n", + " response = requests.get(url, headers=headers)\n", + " soup = BeautifulSoup(response.content, 'html.parser')\n", + " self.title = soup.title.string if soup.title else \"No title found\"\n", + " for irrelevant in soup.body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + " self.text = soup.body.get_text(separator=\"\\n\", strip=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ef960cf-6dc2-4cda-afb3-b38be12f4c97", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's try one out. Change the website and add print statements to follow along.\n", + "\n", + "ed = Website(\"https://edwarddonner.com\")\n", + "print(ed.title)\n", + "print(ed.text)" + ] + }, + { + "cell_type": "markdown", + "id": "6a478a0c-2c53-48ff-869c-4d08199931e1", + "metadata": {}, + "source": [ + "## Types of prompts\n", + "\n", + "You may know this already - but if not, you will get very familiar with it!\n", + "\n", + "Models like GPT4o have been trained to receive instructions in a particular way.\n", + "\n", + "They expect to receive:\n", + "\n", + "**A system prompt** that tells them what task they are performing and what tone they should use\n", + "\n", + "**A user prompt** -- the conversation starter that they should reply to" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abdb8417-c5dc-44bc-9bee-2e059d162699", + "metadata": {}, + "outputs": [], + "source": [ + "# Define our system prompt - you can experiment with this later, changing the last sentence to 'Respond in markdown in Spanish.\"\n", + "\n", + "system_prompt = \"You are an assistant that analyzes the contents of a website \\\n", + "and provides a short summary, ignoring text that might be navigation related. \\\n", + "Respond in markdown.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0275b1b-7cfe-4f9d-abfa-7650d378da0c", + "metadata": {}, + "outputs": [], + "source": [ + "# A function that writes a User Prompt that asks for summaries of websites:\n", + "\n", + "def user_prompt_for(website):\n", + " user_prompt = f\"You are looking at a website titled {website.title}\"\n", + " user_prompt += \"\\nThe contents of this website is as follows; \\\n", + "please provide a short summary of this website in markdown. \\\n", + "If it includes news or announcements, then summarize these too.\\n\\n\"\n", + " user_prompt += website.text\n", + " return user_prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26448ec4-5c00-4204-baec-7df91d11ff2e", + "metadata": {}, + "outputs": [], + "source": [ + "print(user_prompt_for(ed))" + ] + }, + { + "cell_type": "markdown", + "id": "ea211b5f-28e1-4a86-8e52-c0b7677cadcc", + "metadata": {}, + "source": [ + "## Messages\n", + "\n", + "Similar to OPENAI GROQ APIs share this structure:\n", + "\n", + "```\n", + "[\n", + " {\"role\": \"system\", \"content\": \"system message goes here\"},\n", + " {\"role\": \"user\", \"content\": \"user message goes here\"}\n", + "]\n", + "\n", + "To give you a preview, the next 2 cells make a rather simple call - we won't stretch the might GPT (yet!)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f25dcd35-0cd0-4235-9f64-ac37ed9eaaa5", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a snarky assistant\"},\n", + " {\"role\": \"user\", \"content\": \"What is 2 + 2?\"}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21ed95c5-7001-47de-a36d-1d6673b403ce", + "metadata": {}, + "outputs": [], + "source": [ + "# To give you a preview -- calling Groq with system and user messages:\n", + "\n", + "response = groq.chat.completions.create(model=\"llama-3.3-70b-versatile\", messages=messages)\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "d06e8d78-ce4c-4b05-aa8e-17050c82bb47", + "metadata": {}, + "source": [ + "## And now let's build useful messages for LLAMA3.3, using a function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0134dfa4-8299-48b5-b444-f2a8c3403c88", + "metadata": {}, + "outputs": [], + "source": [ + "# See how this function creates exactly the format above\n", + "\n", + "def messages_for(website):\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt_for(website)}\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36478464-39ee-485c-9f3f-6a4e458dbc9c", + "metadata": {}, + "outputs": [], + "source": [ + "# Try this out, and then try for a few more websites\n", + "\n", + "messages_for(ed)" + ] + }, + { + "cell_type": "markdown", + "id": "16f49d46-bf55-4c3e-928f-68fc0bf715b0", + "metadata": {}, + "source": [ + "## Time to bring it together - the API for GROQ is very simple!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "905b9919-aba7-45b5-ae65-81b3d1d78e34", + "metadata": {}, + "outputs": [], + "source": [ + "# And now: call the GROQ API\n", + "\n", + "def summarize(url):\n", + " website = Website(url)\n", + " response = groq.chat.completions.create(\n", + " model = \"llama-3.3-70b-versatile\",\n", + " messages = messages_for(website)\n", + " )\n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e38d41-dfa4-4b20-9c96-c46ea75d9fb5", + "metadata": {}, + "outputs": [], + "source": [ + "summarize(\"https://edwarddonner.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d926d59-450e-4609-92ba-2d6f244f1342", + "metadata": {}, + "outputs": [], + "source": [ + "# A function to display this nicely in the Jupyter output, using markdown\n", + "\n", + "def display_summary(url):\n", + " summary = summarize(url)\n", + " display(Markdown(summary))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3018853a-445f-41ff-9560-d925d1774b2f", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://edwarddonner.com\")" + ] + }, + { + "cell_type": "markdown", + "id": "b3bcf6f4-adce-45e9-97ad-d9a5d7a3a624", + "metadata": {}, + "source": [ + "# Let's try more websites\n", + "\n", + "Note that this will only work on websites that can be scraped using this simplistic approach.\n", + "\n", + "Websites that are rendered with Javascript, like React apps, won't show up. See the community-contributions folder for a Selenium implementation that gets around this. You'll need to read up on installing Selenium (ask ChatGPT!)\n", + "\n", + "Also Websites protected with CloudFront (and similar) may give 403 errors - many thanks Andy J for pointing this out.\n", + "\n", + "But many websites will work just fine!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45d83403-a24c-44b5-84ac-961449b4008f", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://cnn.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75e9fd40-b354-4341-991e-863ef2e59db7", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://anthropic.com\")" + ] + }, + { + "cell_type": "markdown", + "id": "c951be1a-7f1b-448f-af1f-845978e47e2c", + "metadata": {}, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Business applications

\n", + " In this exercise, you experienced calling the Cloud API of a Frontier Model (a leading model at the frontier of AI) for the first time. We will be using APIs like OpenAI at many stages in the course, in addition to building our own LLMs.\n", + "\n", + "More specifically, we've applied this to Summarization - a classic Gen AI use case to make a summary. This can be applied to any business vertical - summarizing the news, summarizing financial performance, summarizing a resume in a cover letter - the applications are limitless. Consider how you could apply Summarization in your business, and try prototyping a solution.\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Before you continue - now try yourself

\n", + " Use the cell below to make your own simple commercial example. Stick with the summarization use case for now. Here's an idea: write something that will take the contents of an email, and will suggest an appropriate short subject line for the email. That's the kind of feature that might be built into a commercial email tool.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00743dac-0e70-45b7-879a-d7293a6f68a6", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 1: Create your prompts\n", + "\n", + "system_prompt = \"something here\"\n", + "user_prompt = \"\"\"\n", + " Lots of text\n", + " Can be pasted here\n", + "\"\"\"\n", + "\n", + "# Step 2: Make the messages list\n", + "\n", + "messages = [] # fill this in\n", + "\n", + "# Step 3: Call OpenAI\n", + "\n", + "response =\n", + "\n", + "# Step 4: print the result\n", + "\n", + "print(" + ] + }, + { + "cell_type": "markdown", + "id": "36ed9f14-b349-40e9-a42c-b367e77f8bda", + "metadata": {}, + "source": [ + "## An extra exercise for those who enjoy web scraping\n", + "\n", + "You may notice that if you try `display_summary(\"https://openai.com\")` - it doesn't work! That's because OpenAI has a fancy website that uses Javascript. There are many ways around this that some of you might be familiar with. For example, Selenium is a hugely popular framework that runs a browser behind the scenes, renders the page, and allows you to query it. If you have experience with Selenium, Playwright or similar, then feel free to improve the Website class to use them. In the community-contributions folder, you'll find an example Selenium solution from a student (thank you!)" + ] + }, + { + "cell_type": "markdown", + "id": "eeab24dc-5f90-4570-b542-b0585aca3eb6", + "metadata": {}, + "source": [ + "# Sharing your code\n", + "\n", + "I'd love it if you share your code afterwards so I can share it with others! You'll notice that some students have already made changes (including a Selenium implementation) which you will find in the community-contributions folder. If you'd like add your changes to that folder, submit a Pull Request with your new versions in that folder and I'll merge your changes.\n", + "\n", + "If you're not an expert with git (and I am not!) then GPT has given some nice instructions on how to submit a Pull Request. It's a bit of an involved process, but once you've done it once it's pretty clear. As a pro-tip: it's best if you clear the outputs of your Jupyter notebooks (Edit >> Clean outputs of all cells, and then Save) for clean notebooks.\n", + "\n", + "Here are good instructions courtesy of an AI friend: \n", + "https://chatgpt.com/share/677a9cb5-c64c-8012-99e0-e06e88afd293" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 28564eabb4443b646e923b62ee0f5ca24cd6e897 Mon Sep 17 00:00:00 2001 From: Dipin Date: Fri, 31 Jan 2025 08:48:45 -0600 Subject: [PATCH 02/10] Added my contributions to community-contributions --- .../day1-Groq-API.ipynb | 530 ++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 week1/community-contributions/day1-Groq-API.ipynb diff --git a/week1/community-contributions/day1-Groq-API.ipynb b/week1/community-contributions/day1-Groq-API.ipynb new file mode 100644 index 0000000..3838097 --- /dev/null +++ b/week1/community-contributions/day1-Groq-API.ipynb @@ -0,0 +1,530 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d15d8294-3328-4e07-ad16-8a03e9bbfdb9", + "metadata": {}, + "source": [ + "## DAY1 LLM Project with GROQ!\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e2a9393-7767-488e-a8bf-27c12dca35bd", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import requests\n", + "from dotenv import load_dotenv\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display\n", + "from groq import Groq\n", + "\n", + "# If you get an error running this cell, then please head over to the troubleshooting notebook!" + ] + }, + { + "cell_type": "markdown", + "id": "5d899ad6-1428-481b-b308-750308d80442", + "metadata": {}, + "source": [ + "If you are getting error ModuleNotFoundError: No module named 'groq' follow below steps.\n", + "\n", + "1. Activate llms enviornment from Anaconda, so that (llms) is showing in your prompt, as this is the environment where the package will get installed.Install pip here. \n", + "\n", + "(base) PS C:\\Users\\test\\OneDrive\\Desktop\\AI\\projects\\llm_engineering> conda activate llms\n", + "(llms) PS C:\\Users\\test\\OneDrive\\Desktop\\AI\\projects\\llm_engineering> pip install groq\n", + "\n", + "\n", + "2. After you install a new package, you'd need to restart the Kernel in jupyter lab for each notebook (Kernel >> Restart Kernel and Clear Values Of All Outputs).\n", + "\n", + "You can also run this command in jupyter lab to see whether it's installed:\n", + "\n", + "!pip show groq\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99c0c3c9-fa5e-405e-8453-2a557dc60c09", + "metadata": {}, + "outputs": [], + "source": [ + "!pip show groq" + ] + }, + { + "cell_type": "markdown", + "id": "6900b2a8-6384-4316-8aaa-5e519fca4254", + "metadata": {}, + "source": [ + "# Connecting to GROQ\n", + "\n", + "The next cell is where we load in the environment variables in your `.env` file and connect to GROQ.\n", + "\n", + ".env file should have below entry\n", + "\n", + "GROQ_API_KEY=gsk_xxxxxx\n", + "\n", + "GROQ keys can be configired by logging to below link\n", + "https://console.groq.com/keys\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b87cadb-d513-4303-baee-a37b6f938e4d", + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment variables in a file called .env\n", + "\n", + "load_dotenv(override=True)\n", + "api_key = os.getenv('GROQ_API_KEY')\n", + "\n", + "# Check the key\n", + "\n", + "if not api_key:\n", + " print(\"No API key was found - please head over to the troubleshooting notebook in this folder to identify & fix!\")\n", + "elif not api_key.startswith(\"gsk_\"):\n", + " print(\"An API key was found, but it doesn't start sk-proj-; please check you're using the right key - see troubleshooting notebook\")\n", + "elif api_key.strip() != api_key:\n", + " print(\"An API key was found, but it looks like it might have space or tab characters at the start or end - please remove them - see troubleshooting notebook\")\n", + "else:\n", + " print(\"API key found and looks good so far!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "019974d9-f3ad-4a8a-b5f9-0a3719aea2d3", + "metadata": {}, + "outputs": [], + "source": [ + "groq = Groq()" + ] + }, + { + "cell_type": "markdown", + "id": "442fc84b-0815-4f40-99ab-d9a5da6bda91", + "metadata": {}, + "source": [ + "# Let's make a quick call to a Frontier model to get started, as a preview!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a58394bf-1e45-46af-9bfd-01e24da6f49a", + "metadata": {}, + "outputs": [], + "source": [ + "# To give you a preview -- calling Groq with these messages is this easy. Any problems, head over to the Troubleshooting notebook.\n", + "\n", + "message = \"Hello, GPT! This is my first ever message to you! Hi!\"\n", + "response = groq.chat.completions.create(model=\"llama-3.3-70b-versatile\", messages=[{\"role\":\"user\", \"content\":message}])\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "2aa190e5-cb31-456a-96cc-db109919cd78", + "metadata": {}, + "source": [ + "## OK onwards with our first project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5e793b2-6775-426a-a139-4848291d0463", + "metadata": {}, + "outputs": [], + "source": [ + "# A class to represent a Webpage\n", + "# If you're not familiar with Classes, check out the \"Intermediate Python\" notebook\n", + "\n", + "# Some websites need you to use proper headers when fetching them:\n", + "headers = {\n", + " \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\"\n", + "}\n", + "\n", + "class Website:\n", + "\n", + " def __init__(self, url):\n", + " \"\"\"\n", + " Create this Website object from the given url using the BeautifulSoup library\n", + " \"\"\"\n", + " self.url = url\n", + " response = requests.get(url, headers=headers)\n", + " soup = BeautifulSoup(response.content, 'html.parser')\n", + " self.title = soup.title.string if soup.title else \"No title found\"\n", + " for irrelevant in soup.body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + " self.text = soup.body.get_text(separator=\"\\n\", strip=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ef960cf-6dc2-4cda-afb3-b38be12f4c97", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's try one out. Change the website and add print statements to follow along.\n", + "\n", + "ed = Website(\"https://edwarddonner.com\")\n", + "print(ed.title)\n", + "print(ed.text)" + ] + }, + { + "cell_type": "markdown", + "id": "6a478a0c-2c53-48ff-869c-4d08199931e1", + "metadata": {}, + "source": [ + "## Types of prompts\n", + "\n", + "You may know this already - but if not, you will get very familiar with it!\n", + "\n", + "Models like GPT4o have been trained to receive instructions in a particular way.\n", + "\n", + "They expect to receive:\n", + "\n", + "**A system prompt** that tells them what task they are performing and what tone they should use\n", + "\n", + "**A user prompt** -- the conversation starter that they should reply to" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abdb8417-c5dc-44bc-9bee-2e059d162699", + "metadata": {}, + "outputs": [], + "source": [ + "# Define our system prompt - you can experiment with this later, changing the last sentence to 'Respond in markdown in Spanish.\"\n", + "\n", + "system_prompt = \"You are an assistant that analyzes the contents of a website \\\n", + "and provides a short summary, ignoring text that might be navigation related. \\\n", + "Respond in markdown.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0275b1b-7cfe-4f9d-abfa-7650d378da0c", + "metadata": {}, + "outputs": [], + "source": [ + "# A function that writes a User Prompt that asks for summaries of websites:\n", + "\n", + "def user_prompt_for(website):\n", + " user_prompt = f\"You are looking at a website titled {website.title}\"\n", + " user_prompt += \"\\nThe contents of this website is as follows; \\\n", + "please provide a short summary of this website in markdown. \\\n", + "If it includes news or announcements, then summarize these too.\\n\\n\"\n", + " user_prompt += website.text\n", + " return user_prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26448ec4-5c00-4204-baec-7df91d11ff2e", + "metadata": {}, + "outputs": [], + "source": [ + "print(user_prompt_for(ed))" + ] + }, + { + "cell_type": "markdown", + "id": "ea211b5f-28e1-4a86-8e52-c0b7677cadcc", + "metadata": {}, + "source": [ + "## Messages\n", + "\n", + "Similar to OPENAI GROQ APIs share this structure:\n", + "\n", + "```\n", + "[\n", + " {\"role\": \"system\", \"content\": \"system message goes here\"},\n", + " {\"role\": \"user\", \"content\": \"user message goes here\"}\n", + "]\n", + "\n", + "To give you a preview, the next 2 cells make a rather simple call - we won't stretch the might GPT (yet!)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f25dcd35-0cd0-4235-9f64-ac37ed9eaaa5", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a snarky assistant\"},\n", + " {\"role\": \"user\", \"content\": \"What is 2 + 2?\"}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21ed95c5-7001-47de-a36d-1d6673b403ce", + "metadata": {}, + "outputs": [], + "source": [ + "# To give you a preview -- calling Groq with system and user messages:\n", + "\n", + "response = groq.chat.completions.create(model=\"llama-3.3-70b-versatile\", messages=messages)\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "d06e8d78-ce4c-4b05-aa8e-17050c82bb47", + "metadata": {}, + "source": [ + "## And now let's build useful messages for LLAMA3.3, using a function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0134dfa4-8299-48b5-b444-f2a8c3403c88", + "metadata": {}, + "outputs": [], + "source": [ + "# See how this function creates exactly the format above\n", + "\n", + "def messages_for(website):\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt_for(website)}\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36478464-39ee-485c-9f3f-6a4e458dbc9c", + "metadata": {}, + "outputs": [], + "source": [ + "# Try this out, and then try for a few more websites\n", + "\n", + "messages_for(ed)" + ] + }, + { + "cell_type": "markdown", + "id": "16f49d46-bf55-4c3e-928f-68fc0bf715b0", + "metadata": {}, + "source": [ + "## Time to bring it together - the API for GROQ is very simple!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "905b9919-aba7-45b5-ae65-81b3d1d78e34", + "metadata": {}, + "outputs": [], + "source": [ + "# And now: call the GROQ API\n", + "\n", + "def summarize(url):\n", + " website = Website(url)\n", + " response = groq.chat.completions.create(\n", + " model = \"llama-3.3-70b-versatile\",\n", + " messages = messages_for(website)\n", + " )\n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e38d41-dfa4-4b20-9c96-c46ea75d9fb5", + "metadata": {}, + "outputs": [], + "source": [ + "summarize(\"https://edwarddonner.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d926d59-450e-4609-92ba-2d6f244f1342", + "metadata": {}, + "outputs": [], + "source": [ + "# A function to display this nicely in the Jupyter output, using markdown\n", + "\n", + "def display_summary(url):\n", + " summary = summarize(url)\n", + " display(Markdown(summary))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3018853a-445f-41ff-9560-d925d1774b2f", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://edwarddonner.com\")" + ] + }, + { + "cell_type": "markdown", + "id": "b3bcf6f4-adce-45e9-97ad-d9a5d7a3a624", + "metadata": {}, + "source": [ + "# Let's try more websites\n", + "\n", + "Note that this will only work on websites that can be scraped using this simplistic approach.\n", + "\n", + "Websites that are rendered with Javascript, like React apps, won't show up. See the community-contributions folder for a Selenium implementation that gets around this. You'll need to read up on installing Selenium (ask ChatGPT!)\n", + "\n", + "Also Websites protected with CloudFront (and similar) may give 403 errors - many thanks Andy J for pointing this out.\n", + "\n", + "But many websites will work just fine!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45d83403-a24c-44b5-84ac-961449b4008f", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://cnn.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75e9fd40-b354-4341-991e-863ef2e59db7", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://anthropic.com\")" + ] + }, + { + "cell_type": "markdown", + "id": "c951be1a-7f1b-448f-af1f-845978e47e2c", + "metadata": {}, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Business applications

\n", + " In this exercise, you experienced calling the Cloud API of a Frontier Model (a leading model at the frontier of AI) for the first time. We will be using APIs like OpenAI at many stages in the course, in addition to building our own LLMs.\n", + "\n", + "More specifically, we've applied this to Summarization - a classic Gen AI use case to make a summary. This can be applied to any business vertical - summarizing the news, summarizing financial performance, summarizing a resume in a cover letter - the applications are limitless. Consider how you could apply Summarization in your business, and try prototyping a solution.\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Before you continue - now try yourself

\n", + " Use the cell below to make your own simple commercial example. Stick with the summarization use case for now. Here's an idea: write something that will take the contents of an email, and will suggest an appropriate short subject line for the email. That's the kind of feature that might be built into a commercial email tool.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00743dac-0e70-45b7-879a-d7293a6f68a6", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 1: Create your prompts\n", + "\n", + "system_prompt = \"something here\"\n", + "user_prompt = \"\"\"\n", + " Lots of text\n", + " Can be pasted here\n", + "\"\"\"\n", + "\n", + "# Step 2: Make the messages list\n", + "\n", + "messages = [] # fill this in\n", + "\n", + "# Step 3: Call OpenAI\n", + "\n", + "response =\n", + "\n", + "# Step 4: print the result\n", + "\n", + "print(" + ] + }, + { + "cell_type": "markdown", + "id": "36ed9f14-b349-40e9-a42c-b367e77f8bda", + "metadata": {}, + "source": [ + "## An extra exercise for those who enjoy web scraping\n", + "\n", + "You may notice that if you try `display_summary(\"https://openai.com\")` - it doesn't work! That's because OpenAI has a fancy website that uses Javascript. There are many ways around this that some of you might be familiar with. For example, Selenium is a hugely popular framework that runs a browser behind the scenes, renders the page, and allows you to query it. If you have experience with Selenium, Playwright or similar, then feel free to improve the Website class to use them. In the community-contributions folder, you'll find an example Selenium solution from a student (thank you!)" + ] + }, + { + "cell_type": "markdown", + "id": "eeab24dc-5f90-4570-b542-b0585aca3eb6", + "metadata": {}, + "source": [ + "# Sharing your code\n", + "\n", + "I'd love it if you share your code afterwards so I can share it with others! You'll notice that some students have already made changes (including a Selenium implementation) which you will find in the community-contributions folder. If you'd like add your changes to that folder, submit a Pull Request with your new versions in that folder and I'll merge your changes.\n", + "\n", + "If you're not an expert with git (and I am not!) then GPT has given some nice instructions on how to submit a Pull Request. It's a bit of an involved process, but once you've done it once it's pretty clear. As a pro-tip: it's best if you clear the outputs of your Jupyter notebooks (Edit >> Clean outputs of all cells, and then Save) for clean notebooks.\n", + "\n", + "Here are good instructions courtesy of an AI friend: \n", + "https://chatgpt.com/share/677a9cb5-c64c-8012-99e0-e06e88afd293" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 24a9e1a606e516aa03db3ded3bffd61bb15235c9 Mon Sep 17 00:00:00 2001 From: Emads Date: Fri, 31 Jan 2025 17:20:06 +0200 Subject: [PATCH 03/10] Add contributions to week6 community-contributions --- .../ems_week6_day4_gemini_results.ipynb | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 week6/community-contributions/ems_week6_day4_gemini_results.ipynb diff --git a/week6/community-contributions/ems_week6_day4_gemini_results.ipynb b/week6/community-contributions/ems_week6_day4_gemini_results.ipynb new file mode 100644 index 0000000..dd8b448 --- /dev/null +++ b/week6/community-contributions/ems_week6_day4_gemini_results.ipynb @@ -0,0 +1,313 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "db8736a7-ed94-441c-9556-831fa57b5a10", + "metadata": {}, + "source": [ + "# The Product Pricer Continued...\n", + "\n", + "## Testing Gemini-1.5-pro model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "681c717b-4c24-4ac3-a5f3-3c5881d6e70a", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "from dotenv import load_dotenv\n", + "import matplotlib.pyplot as plt\n", + "import pickle\n", + "import google.generativeai as google_genai\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21a3833e-4093-43b0-8f7b-839c50b911ea", + "metadata": {}, + "outputs": [], + "source": [ + "from items import Item\n", + "from testing import Tester " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36d05bdc-0155-4c72-a7ee-aa4e614ffd3c", + "metadata": {}, + "outputs": [], + "source": [ + "# environment\n", + "load_dotenv()\n", + "os.environ['GOOGLE_API_KEY'] = os.getenv('GOOGLE_API_KEY', 'your-key-if-not-using-env')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0a6fb86-74a4-403c-ab25-6db2d74e9d2b", + "metadata": {}, + "outputs": [], + "source": [ + "google_genai.configure()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c830ed3e-24ee-4af6-a07b-a1bfdcd39278", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c9b05f4-c9eb-462c-8d86-de9140a2d985", + "metadata": {}, + "outputs": [], + "source": [ + "# Load in the pickle files that are located in the `pickled_dataset` folder\n", + "with open('train.pkl', 'rb') as file:\n", + " train = pickle.load(file)\n", + "\n", + "with open('test.pkl', 'rb') as file:\n", + " test = pickle.load(file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc5c807b-c14c-458e-8cca-32bc0cc5b7c3", + "metadata": {}, + "outputs": [], + "source": [ + "# Function to create the messages format required for Gemini 1.5 Pro\n", + "# This function prepares the system and user messages in the format expected by Gemini models.\n", + "def gemini_messages_for(item):\n", + " system_message = \"You estimate prices of items. Reply only with the price, no explanation\"\n", + " \n", + " # Modify the test prompt by removing \"to the nearest dollar\" and \"Price is $\"\n", + " # This ensures that the model receives a cleaner, simpler prompt.\n", + " user_prompt = item.test_prompt().replace(\" to the nearest dollar\", \"\").replace(\"\\n\\nPrice is $\", \"\")\n", + "\n", + " # Reformat messages to Gemini’s expected format: messages = [{'role':'user', 'parts': ['hello']}]\n", + " return [\n", + " {\"role\": \"system\", \"parts\": [system_message]}, # System-level instruction\n", + " {\"role\": \"user\", \"parts\": [user_prompt]}, # User's query\n", + " {\"role\": \"model\", \"parts\": [\"Price is $\"]} # Assistant's expected prefix for response\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6da66bb-bc4b-49ad-9224-a388470ef20b", + "metadata": {}, + "outputs": [], + "source": [ + "# Example usage of the gemini_messages_for function\n", + "gemini_messages_for(test[0]) # Generate message structure for the first test item" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1af1888-f94a-4106-b0d8-8a70939eec4e", + "metadata": {}, + "outputs": [], + "source": [ + "# Utility function to extract the numerical price from a given string\n", + "# This function removes currency symbols and commas, then extracts the first number found.\n", + "def get_price(s):\n", + " s = s.replace('$', '').replace(',', '') # Remove currency symbols and formatting\n", + " match = re.search(r\"[-+]?\\d*\\.\\d+|\\d+\", s) # Regular expression to find a number\n", + " return float(match.group()) if match else 0 # Convert matched value to float, return 0 if no match" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a053c1a9-f86e-427c-a6be-ed8ec7bd63a5", + "metadata": {}, + "outputs": [], + "source": [ + "# Example usage of get_price function\n", + "get_price(\"The price is roughly $99.99 because blah blah\") # Expected output: 99.99" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34a88e34-1719-4d08-adbe-adb69dfe5e83", + "metadata": {}, + "outputs": [], + "source": [ + "# Function to get the estimated price using Gemini 1.5 Pro\n", + "def gemini_1_point_5_pro(item):\n", + " messages = gemini_messages_for(item) # Generate messages for the model\n", + " system_message = messages[0]['parts'][0] # Extract system-level instruction\n", + " user_messages = messages[1:] # Remove system message from messages list\n", + " \n", + " # Initialize Gemini 1.5 Pro model with system instruction\n", + " gemini = google_genai.GenerativeModel(\n", + " model_name=\"gemini-1.5-pro\",\n", + " system_instruction=system_message\n", + " )\n", + "\n", + " # Generate response using Gemini API\n", + " response = gemini.generate_content(\n", + " contents=user_messages,\n", + " generation_config=google_genai.GenerationConfig(max_output_tokens=5)\n", + " )\n", + "\n", + " # Extract text response and convert to numerical price\n", + " return get_price(response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d89b10bb-8ebb-42ef-9146-f6e64e6849f9", + "metadata": {}, + "outputs": [], + "source": [ + "# Example usage:\n", + "gemini_1_point_5_pro(test[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89ad07e6-a28a-4625-b61e-d2ce12d440fc", + "metadata": {}, + "outputs": [], + "source": [ + "# Retrieve the actual price of the test item (for comparison)\n", + "test[0].price # Output: 374.41" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "384f28e5-e51f-4cd3-8d74-30a8275530db", + "metadata": {}, + "outputs": [], + "source": [ + "# Test the function for gemini-1.5 pro using the Tester framework\n", + "Tester.test(gemini_1_point_5_pro, test)" + ] + }, + { + "cell_type": "markdown", + "id": "9b627291-b02e-48dd-9130-703498135ddf", + "metadata": {}, + "source": [ + "## Five, Gemini-2.0-flash" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ee393a9-7afd-404f-92f2-a64bb4d5fb8b", + "metadata": {}, + "outputs": [], + "source": [ + "# Function to get the estimated price using Gemini-2.0-flash-exp\n", + "def gemini_2_point_0_flash_exp(item):\n", + " messages = gemini_messages_for(item) # Generate messages for the model\n", + " system_message = messages[0]['parts'][0] # Extract system-level instruction\n", + " user_messages = messages[1:] # Remove system message from messages list\n", + " \n", + " # Initialize Gemini-2.0-flash-exp model with system instruction\n", + " gemini = google_genai.GenerativeModel(\n", + " model_name=\"gemini-2.0-flash-exp\",\n", + " system_instruction=system_message\n", + " )\n", + "\n", + " # Adding a delay to avoid hitting the API rate limit and getting a \"ResourceExhausted: 429\" error\n", + " time.sleep(5)\n", + " \n", + " # Generate response using Gemini API\n", + " response = gemini.generate_content(\n", + " contents=user_messages,\n", + " generation_config=google_genai.GenerationConfig(max_output_tokens=5)\n", + " )\n", + "\n", + " # Extract text response and convert to numerical price\n", + " return get_price(response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "203dc6f1-309e-46eb-9957-e06eed803cc8", + "metadata": {}, + "outputs": [], + "source": [ + "# Example usage:\n", + "gemini_2_point_0_flash_exp(test[0]) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a844df09-d347-40b9-bb79-006ec4160aab", + "metadata": {}, + "outputs": [], + "source": [ + "# Retrieve the actual price of the test item (for comparison)\n", + "test[0].price # Output: 374.41" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "500b45c7-e5c1-44f2-95c9-1c3c06365339", + "metadata": {}, + "outputs": [], + "source": [ + "# Test the function for gemini-2.0-flash-exp using the Tester framework\n", + "Tester.test(gemini_2_point_0_flash_exp, test)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "746b2d12-ba92-48e2-9065-c9a108d1593b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 00e6d3ec388bc1755f541cbc72f12bc3a868af5d Mon Sep 17 00:00:00 2001 From: Nicholas Arquette Date: Fri, 31 Jan 2025 18:20:37 -0600 Subject: [PATCH 04/10] Feature: Added a chat RAG example with sample medical provide note data supplied from mtsamples.com. --- .../rag_chat_example/README.md | 37 +++ .../rag_chat_example/img.png | Bin 0 -> 90717 bytes .../test_patient_1_f/progress_note.txt | 44 +++ .../test_patient_2_f/progress_note.txt | 50 ++++ .../test_patient_3_m/progress_note.txt | 25 ++ .../test_patient_4_f/progress_note.txt | 54 ++++ .../rag_chat_example/run_rag_chat.py | 59 ++++ .../rag_chat_example/utils.py | 267 ++++++++++++++++++ 8 files changed, 536 insertions(+) create mode 100644 week5/community-contributions/rag_chat_example/README.md create mode 100644 week5/community-contributions/rag_chat_example/img.png create mode 100644 week5/community-contributions/rag_chat_example/knowledge_base/mtsample_dictations/test_patient_1_f/progress_note.txt create mode 100644 week5/community-contributions/rag_chat_example/knowledge_base/mtsample_dictations/test_patient_2_f/progress_note.txt create mode 100644 week5/community-contributions/rag_chat_example/knowledge_base/mtsample_dictations/test_patient_3_m/progress_note.txt create mode 100644 week5/community-contributions/rag_chat_example/knowledge_base/mtsample_dictations/test_patient_4_f/progress_note.txt create mode 100644 week5/community-contributions/rag_chat_example/run_rag_chat.py create mode 100644 week5/community-contributions/rag_chat_example/utils.py diff --git a/week5/community-contributions/rag_chat_example/README.md b/week5/community-contributions/rag_chat_example/README.md new file mode 100644 index 0000000..7f92b0c --- /dev/null +++ b/week5/community-contributions/rag_chat_example/README.md @@ -0,0 +1,37 @@ +# Overview + +This uses de-identified medical dictation data supplied by [mtsamples](https://mtsamples.com). The data from the mtsamples +website was download from [kaggle](https://www.kaggle.com/datasets/tboyle10/medicaltranscriptions). There are four +sample notes in different directories (see knowledge_base/mtsamples_dictations) that will added to a chromaDb +vector database and will be available during chat using RAG (Retrieval Augmented Generation). + +# How to run + +- Run example + +```shell +conda activate +cd +python run_rag_chat.py +``` + +# Chat example + +![Chat Example](img.png) + +# Questions to ask? + +1) How old is Ms. Connor? +2) What are Ms. Connor's vital signs? +3) How old is Ms. Mouse? +4) What is Ms. Mouse concerned about? +5) What are Ms. Mouse's vital signs? +6) How old is Mr. Duck? +7) Why did Mr. Duck go to the doctor? +8) How old is Ms. Barbara? +9) Why did Ms. Barbara go to the doctor? +10) Is Ms. Barbara allergic to anything? + + + + diff --git a/week5/community-contributions/rag_chat_example/img.png b/week5/community-contributions/rag_chat_example/img.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b2ba7fce0159f5325ac56f8812483966ad6a17 GIT binary patch literal 90717 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%0DVg%|WdUg8SW3 z^jVWQE^Ckif?Mw|EJXPC7nAZ)i#gNYdicRoB3TE8M~g1d;E(IN{U~yGH}^#?@Hf3{ zzmBc%0a04BUQXzJW(-#JlVtghhcuIWwC97TiIUdN4jtKXGTjBvmK`tx(c>$EKH#DYnlD{JP*a&lz57hB&5h<(|~_s=VoTe`kA2req8##LH- zWj7nZyk;K63eiRD`7DlUe}S75QLe|G*eL%aLx9=}YqZ4c%!U#XSg;EU1sHy&1krgJe!;;p>-<|JXjG?)|9gE^-+aTX{;_6`&j2jPW zqIr_WKtSlyhe$hu!oTGnB|rLdg@}HKX?kA-@CF3@qpUO)a-|#QsmgoNdz2cHcny-< zA^Dh~sa{biu1G{A#1&Zwpp791;FmI+;*+fl(Qz^Ow$+xG*unT9V#$FhBEvS@OjkT0AoE|#2+aG@jDS#32gIU1 z^|J^?GJx_hM_Mmi>g51r;y^K0ZZ&Co62zr4Q#HLv^R}T7f>GZA&ul;%Z2=q6$DveX z=)DLhl_qm`m3*W@=}UjbH}O&CpDStmazf2#`k|8FJtG8{R~Eqekyd6kF?Qaxmw@oa zw>z`#@>~%qwd>1ARx==1xE@&713s(}X3&L7s`VC@!Rp~wtza;-1O7d;6}nXvOYYr< zBuI7xtm5{+S$6D zbNcp`3o4jcgwem*=1%vy%I^dlamVQ9`{#<+;ui5sRDX49UW1L`THFtOWpWv~=n2A+ zC6g&*=ULejh5&#I(8~^bNDEh*1GGwgj(}nnrNZ*l8WKHo+X^$Ikse|H!JXA|j>J_h zuxuRFQ76J?4<|=hy-3 z=k=a=yR(9%m;q5=HZE`2@Ygm?RqLdP*#rpdyDO*T@mfRj37Vv-ndwIrn_I0+p+7~x z(0X1WH2lC~|B*G>dsXnqoLTkP5?i-A$JJbQ>^_pk?-`Xz0z91hYVuAaY7O>{I`)|O z0pj!Z%0A>sBVaS;WyP6W*E{I6gvsl^;T0w+s5gDgAysxPWlVe-2;KbJvE{%_>SI zxE>QMQI#T;MydKKJLa1rg z$(tpxiec1yvZRLrqmZx>c0N_V_2XFGj~DYFTAQ@p^#n$HD%5{QOH$y7 zxLRCSO{xtFs{u2=uW5bx{Vfhlstz#04YX2(1$Lj$xBaPe4kNY-)>}K5FOIoHXsddjjWT&GptJ8^NdY zYz>@U=>&qQX(qp}k@q-=yfU*Hz#1eB6cRZf2I!C0>J@O33ak-C$pvxU(G!Uwphm@Z zzhTzJ^xb@8jA^GS7}HOk)MXL_=<(%16@XEvEmKqrP!Nu^KpN)X?FpX*}ud+3M?Y5I1L?_bWcT`p`07}yt-M4G9-05l0l(Nx$lpGbOV zwm8h(g;hUaS5TQm1t@jK{0oa?v#xi>@|JrPXCsu33e5d`w%yzxCAcUwE?TR6D%`XY zNL}#-seI>;dNrqND?%>t&Iqdsr8f2{PXmq2q=%1;C=Fy1!}eoQGoSfMi$iq@TJt1^ zOUI-z_!^+bDeCvKAl77=tH?G1wm;$hA2`k@F_W zN)=a0`;vGUN}r7#z%$B+E8P@CVx&F^HgidYjUzDa+5fw2`G5H)n{fTxJMyp8mAt@- zL^5@tsJ5(r@zYb!P8otYGQctnVE)1Ud|hiw?a|94)`v+rke{E|pZnk+0#-To#j1Om z+8^4Q9X;B?{THLj2sVkFUN(1w)g2aplsKIf0Md_8JJ(vDFDoWpmr#VwU-MF3A!X!r z**jB_{L@VUAZXYRx5u5%8XyXb?3Z!@gbDqT9Wj$GP5Tl8s0zXSjg7xeNYi$k2zL7U z7e43|@&&1r)9G)aBRMyfN)j&;eSZKo{{n;)9Jvbe7SE&g_VIcip9$>RJbrTjR1LeD zXg1K5hCJl(gr=WqsMxl-02G2IxrBVvp3FH_-+7}Q-*2$s=h!doD*OFk4O2vsqlL|N zqj73AAwWZ5Dt(IFEkgjz_;6zvag*JD?v>7Kd?b;~oe#4PJvkhVi-X0^;cf$oPI$vY zlkgLcUIBX_;A{h*oBY!0sHcipLu`{vXw!^I?0HVqRnb2IzBAs4w4y}1Vl^tWF7o&9 zTeAb=^;rgCVL%|nUZzyT(ZLiHZB`n0>=9q11VWa+`zSZApHc~(vHIx{&86vIDdq!U z^CNd?Z2gf~X>i!kZ9?2MEpZ&F_*YwD(WN;Ey?Azc_l(tq@ zVLQD7e)|43+G-VM0Od^M>i~_r#_-p63ZH2%wEi^w+3KW>L=tfzuzV+h6&0=C8OHSa z7a{=X-wx)31S?hbNxc* z2(?>}GNh@#=?c-pXuNJ6pd$;vt;>OfEl<|s{$$RmHkH3~50|*0QyJul*{sHycM6en z3FgN+d~gBQ%LV5j^|!}2Z~Cxa(!;1km-~b^E{w|$oL8p7`Ei(Cc58Utosd&9whr#( zzQYI0t18O?*phcF!ez=1qhFfbeNI6OO46rRqOI|GveqR%4o-6rt-xDb)AaA>Vt-dD zIm%uIx8J_ZD8Qadp+yXb_?o+wcNL{h4wN+3pm3F-uwBB-i-Xc*=~T-1o9sb!RQJl9 zndwsgGfRBcbN3|QYXDuyCs}8D*T*k+*8yZAGz?6uP`>TWbYx2Vp}Dz*{-qJyc!zH5 z;Z#&o%)>!S#D&Q_6NaALCTHc`xVlF(OYKYVW+yVd@q@LLZ0H@?pkwMlfLU5fTkTlp zATnmZQ|$YFPXrDDWnp5uWjwPVKXavz#>O_tCvd}Wt!i`Hs>{_mx5vI*@e;Bn&uz5 z$1-LkI`ORxszC=mWqR;dzE~Eawj^&|9w$PRrLoxiNbf1vs;QN1E%TB0W|9llyq zVuJ*9Rb6pWeFQyYxpsaW1gFPm7b@yHXTA|$`Yu-BLxVwiwr&(}_hk^M;G)sFP=(wG zl!tGaTS--sR(BO7lVwoJmC*@apJIh~Hg$#;<3pZZso=P_VJ=LYaju>vb}%Ey9*RA{ zB{)g={%dA(0wBD>4SWooES{xI(cKafFfN}Ex^e2|ykLs{amoBZNG9#Z;L)TIiKdAK z@I~+LyY7}^t?O)(>PXg&_DEkJ+*&Fc&%bJxFaW^b zdy58iBy|q9eFQlojgR;xB zR@8H*E*HjG7vLv>Y|XA4ZtnO-+}l9O_2vqo8oC49FzaA51y1XwZ3ZoNDb~^DU2{Px z)?UeoCSWAHsz0Beg0K$AYAR5W{te}Oi1H^0b&eq3jomImuNYA@IGuss$`ln`=jvW> zyqzzEU^6_`f16nulc0xQ>eRxA7N7!&qqoqkA7!F{MF1xpdv3U@J}Z zOHgOvg^4kfl)~KEATe>5lX^SA-QU@1H)QHfw~DApE5{H_AoUGqu(SxRs0l`-wgkJP z)Di-r;5+e^jE4BSl;w(K_jGa+;7MO7@H=n*bs`MsNAh0vJf?@Q8HtD-?Ect-j2yvo z9G)8X;6hMj1k;et_6rNw)m~v29ZSM{8;Bcs>yCHMt3a8ldOWosrKud;(|E9;z6QEX zQdF?sZ--@95w!#53ArWo+DeQGY5e32MFV5^sDyrzU_Z?D!$|d-%6kx^q00-GrHx!j>CwQ%1Eq2S-l>O)5 zzEG@m^Vr9}VwUIOr0x8krWII^AqN%SyM*BsHvi~zaC@8np>RG(Fi(gky+phPwl{*e z-T(&;rDoL>(C<;YI4QU7^dgp6IfG$ao2%gKy^)DvqKRwJ3K=X7i1z z$GsAwCo)RJs~!n1G9qMIdT^3lh}t)uav+iW^wkoF?5fVXJ?Zp|=ohWihMU3+qCp85 z1aQb~s}Yk}8Ni!x{G-t@S6<7xI981!nvGdq*25kpTttXy^v`pheg7IsWo!*28imH@ z_#*nX2N>=@R!_?-77RN_GFNrQHK!brvj54_+}oo6l-z&w4|mfC*?}GC&9(X%o&raw z)BMATcs^_PK#a9FKHD_;YO+Hz#u$ zB;)-`=J)gTC3F$yWQodtjLM!us+s)dN~Hn6N1ux76hagv_h(0QAHtwoFDg<73OU_v zIDJR$=+aHBx13Uvx#DHa?>kMfc}A(!+j|`kUhlI|haZATXmWs~A}U)-RVLAKQ16lL zq+wGAB#c@c2g<5!eFRt7hv>Y$hZXu+7wdJ*1pCfNhXWt*&8NDPdmt02g8>NUq#cf) z=C1F9VfO`c4pzcfRT1x?Zq%~hL*0Bnq~1NQi&c>TVIaaqeCiAIywNW>ofUge;iZp=K@?Ch$rEzDIMsqvlr$;?(Mw%MKajZhMb& z^$HsO2@J!j)k};r=;>&$M#kzOc-y+ubKy;N$F7DrA|8Jl@%>*TM$u+r{r~HDbvrdF zB_g^pWKOKO2`<6?UyF`u9py9a@#~Ey4>yb070N0vm;A_BbdV&FTViKcdC;b-4Vx@; zPBC#&UFnSAVvN>cs95YHVrfgqa9C*;gG3&2@mV=I=UqeIy02>8`1GsFqzq>G&cx-d zA^j1bs7Q9AgvxmGS?OPrS=X-;+up9pK8OqMY{(f^ncSP-v48I9J$KegAKom(Hs$r! zTN?ey$YZ4O6P1=Pn~(HKZq3_o^66FoKj=^Yz}xU4$!!9 z+1%Q=ATfH4Luq<-0)h&;$#HhCx_pkf#}katvepato~?H{ck7IFzRp|hf77|taoZx3 zIoP?vx{%8F{0Z(>z$&jup7f5srP*6sax!>$JKZxmhx3l}jt!al_uxQ%O>lEh<8<%~ zU*^mVO25sUZKvlNCFt3y;`*STCbRTtbXRq!E^l|gzt;hqZoEsr3aVsm*`}TyR19PJ z5m<+);m^2BG8D?~AC3wk&#E|kSdGuoeTK$3%rIyCBncSU{0q$GN}_voPX(CL51#vx zYU{NzD%!f!(PyF8&vo4BS1c$=da+gexphEd>A1?PwsXs@eSJBMz5*H^{Vrr^FA*v8 zc)zT*TlSql3+I94@d&9%-RT)e6|vIY;MhgbY2ruk( z^zgzE5egwjJ6s~Enz>pxSoveQSEcr$@Ku*u-?MrSi641HlnaS7m8R_V zsY&-eNHNSiP#(pX+V)|KjpDVRzt4;1o0SA2R#xj>R9KXvrgNIPg|k(f-{#||14^i# zEBGMfd|;Mq@Ye${*AjwZ+Pg7tB91}ykF)%Cg{;9j-NUs|^L6XJ+U-!SK?t!WJAFl3 zcpd{mas58Zky+TA+pc#UR}uYmd*6lC+4LH$Z-3Q^U`{nyPbBFBgiw}SO**1;;jv?V zgGjRTgg03+W zkN`e6h(095&vC+l+6Kvdf)MN~bZifstICSmo$ls~GxJ%PAEZ$-dJ^FWkDKcTfBwx5 z?*)ac1X#4k?tP=O*8ZcV^5xz>QA-JA|21v6#~)EscGI4pvV{yBrT{OQ)V8 z-6w(IOz7>Qlek4<@Pc9V)#~)7DI%_{qmGH1rk0yLVz|F8<)Ew9yW1AxBf1y_9FnqE zj)l;1B5BN!@B3*{KJe;4x^Aw`{+R{=BD_LpcyU5+Ho+^}?injQ4n!*@zDLC^G#eo& zrg^SyCOGiLF1D`jWfV~T!hWottM@ng52B%RR9(yR@yT0?`k6&&;~X`uq@DXFetY=Aeh1(z|2k|(E zQ>y+Qo;s{hr9dZb*QlfprVP7jKQE!U|F|=U1DKFAp~r7VU$2JR!1&(Jk#-EN4)Lgf zvZYNpRsk7-vLV6IzSZNlxUlW?@DilQh#&MwF>=kBtx?%{JD&U&-Re`)l;A!@UHRrd zC_n!Bxm3o~y^E1WtW8X341P+JgxMlmLcZhfF0GNxi8K6oAz&ENBUi3YAof%>^>~m% zqq#jxdmPmX7N$e1rJ1)&W}BGD6fDAj-l?3|wFaQTg1KH6GW_wnQEs2v7E+aLyPr>C zM&9$qgY?&_<>F6sk6ERD(%uDi`B zb@5appfAKv_Ppf|nwBnm zR%DH=Ho2K&CS=Y~8$SO+QHFV3N;wog6?WU_b3x3N4rG_k466MfzKxdJrcl?Ow8Z7Y zvj~t#Y*~; zS2}vKxBqYp^S>iVBTdn{MD@V z2QV~Ap7(^Zx5)McW^wzV#cQlOPPB(JZrk51%y$^pd(5%x6hM}&mk|{wn-0nTkm#ngTxw!|btSQn zP;0{1_WxubUXwoO{X8zs-^5zi{-zLc@lr4tCyuyz*LSt5ochTF#2K5)mD=YdiwTz| ze{8RT5`811^Qd5ig?=&zk8TdH^^ghu)N+t;I}*N7T1lE3OZv%GybCkd#pf9Re2(np z7@F)aKZ%3Qr_TO&|6%{oYz^d95gS;V|B50wvKeGKIlus+8`e3TG=Na0-RQ^ ztk#o_u5bY*r0ws|f|dzb|Bb!(j%sr4zC{sHz=nto=^`SEq97d- zK?UgoDgpv3(xi6-1Qb!+iXhUYMnyss0!VM6xJ4;JYJdO{DS-qC5E4j2!g=EU{Pwrc zId|Oq8{?ik?zs2-7fRl|d7rk{Tyw6;0=%IG&=sg?5j?-FQjEoLEp-xH7_={b0vdYP z)dE38=$Cp27I8CN+uH>YdMJd~jwLV(bFAPxb~h%J+;Dmfpk<1V<6-?2U`Q41P_?F0 z70!Awt#c7`v2H#L@ZDHrRt~Ip8^ah|WA)>l?lWuA(2acN&llR$H-V5u7IV`}J7kw)={%p~X>7plN0)g;Fcc!_sIgqc;w(pd zQI2m8))qj(D?jn9g4U$wpq7aa+@yI&%!6ZkT)3mV%_52d8+rwY(WkMsK!{Nj%3SDj zasXu0Md1#N-robW6Gtcq#IVrz6HDwXG@dSD!7Lb4BupSP`Uu1?%a-vjTu$dfVv>~c#-Ye=hYPeZeB~y zpT50s5OfY2T&%#VU}p6{6v-nhKfUw-83G08!!GZ`ob=4`Xa^mI9ArM5KID+Dde56f z0V3}xa|?w7*!LmYh4<3tk(F08vs<7WKDHb_n<+0Ipx1ZS7VB(l!LooFAs`G_ zTXhxB^rCl^SAuL%NiwK%>d@~)kn8;yu*~X)aU)v`WK0pH;F@r*_FE;ZkX^P=m9sO3O{}gf(KRsPM2W1lc2a;1uzvz zvmf`*BSQ}1bY+>Zi@fXCN3Ph)MZ5-&CW2WNp@947 zEDHvD2DP7SSZT-4S9q)M$)_Uxw1=aE@GjDS@rw7HWC9lR6y?>C0GE4#$wp^yVNJfL z2@dj42z_xGX0#0OEDQgcb)j-Qjo)8r+&*+6t$}g%@rFEJz+0(T6W3WusgWqoeaX|O zEFVXEq+5s%=bkOs#$6#dYWP6}4p``GqW<18?jEtqoy=^awsG;E+@wYN}dBqpZ5yR@@$#=nKmnNLWG{ksgVApCpr zrtoL49g|))+PR$hy>`nKCN}coDg1*z2KH;8T%@lIOHP^p?bq3>>72L6X(N*VET>VvLYuejVN}@(u%l z2;-+a!$YADUJ&0kwN7oNC(an4{X{JWWdkR1w5Pi=qj_dwuJOF$%0s5ZnK5r7z`i7N z%WRV=6%rv)SQC4_$OW-iyG>HSq6{kRh6ucG@P4&$v!?yx!!S)rcxR2FK?WN#C1oi7 zHHNbHgUs)70g?)r{s)F%cLCm+gOUYgWet!Fkj8OQpYqMJeyBEYe7~|#YcEn@_Jk6V zaprNKK$g&}?t^Mo3)e0-2Qre}_-m95;jW)UcM~XUQ7eG~VZ^+uiLhg@%+4YnLResm#1*J#e)?|_2H20HC>6Vm?fGm{zPXb6&zP;Jy;Va% z*MDSN6lzKNaZl<Rj+n2KbXmC)OKnP!Lb5d!vi(uybW*H|qy159*1q1>Yd4L~|kM zypbkK0)yq=Go-ln+V zED{*bxml7ru6ntqOL_)~eFeq!6$*-qgj%B%5(% zxVbsD#f~}I03-tbtUUfZv$QSi`SSQVK2&ELvTAeH3NR@w6DF-?BTIO>3nV zXbV`cfrfUc6>b*=E|Qju;q??u#q>?Ui6aalz1>uqS37=wP>2ZsevZ2PiLnv1jh|q7 zhH-W%OJafSc!NGSc5H9UB%bk%$QPweO86T5yE_d~eliPG*WqAMN|r=ffWw zYGNHm+CezqW%&F2J*b%xZLs$*kp=I839V@Ee+f3O3S#_E{MY`VJ`rVE8^I<-qzZzV z?9%U#h=qh)UYnHI^w1mP=N0;WdG(DY>GoOruPE`6tf>6&h=~A{4OYP|{IV&6tEX<9 z@NI3@|JQ{Q8|htWVl1EqDn5tk$ch@rYI;q_xQ9giYbN}$&XJg?xmI8)?0x@Vl#TpX zqWz(=Jp=*G`K3i5(5@R>LvOa0i2)kgn!x5#>#y`{Q3S6R_N9TdhKvo_aU&pdE}5Fx zn26r&UO`aM%sO(QzUVSmX$El2kkYa1_%T2YM~(7Mg}==d0B=et0WuQruu0p#<}ghG zuAAb160ew#!V-!zIqBOAPwS@?`bp4kwxVy#!)cikX)3X73zr}C zRd9FtzkeHkZiI(xkLlu0@cx@Sz}9u!+TQ`ZZnEG^!;Um2CzDI&q!~8{M-PG5CHujo zX)2p(1g{_FaN_=C<+W|#2l;JamX&<0Q2?)R%kdFX9s#xr(W`DDcEv^0`*o}akzDtS zZiiGF;Sk*D>_%J#iNe~`D6}DaVUtC!N_@zEsj8BEHnc6)ZVYT; z>W*}}+TIp=OxorcIcpy1yOV_}Gq0m1fQWjr5|9BoS`$tj8-!|Pgt#eClx#Ktpt{Hj zVmVu2AczzRF)DkuP4bW@<+nBgra0%>Jbc1=HZ2%{-G0E;)5jY0oE?A*(imWD3mS*~hdnsu=~n{$-jDVY(Z3 zaVd4JaR!s?U?fLWqWMcyGHUuCqLLJIoU**>vwFbelG)XLWie_Eq)U|Ba>ilh&qFsBeoky;CA3I@3dSicZ$=yWj0YiIEf*(`Z!1`yuV%=xe%@ruhc&kmr> zjPmTTnzSK_K#m^7`V4-bD*42AEK!`sIh`6a;WTvzG6_EIKVs~f1(z7XI>uV} zfFn+GEISUO($K)6yfK&mJQUhq55Vo|7^=-LDj5U`jv3`QS*J71z9Jx1)FCV?qH$Ha zVrddkVHBA0EQ8RBTjsR6vX0_e74LA=s%n`%O`7s4qUNAtFM1IRDo&>f$C=5~N~bB| z8hM;qgYwW~rJ8(qj~)MBlE0@Hn? zmZuAZ!Rvz;w6=t_fWVw0#FOjIPweY1PYV2`;Kr<{oH;=%vTs0w_~4!YOsY#TNM`QB zN_^Voa#QGYR=Ok)g?B7rmkjH{E^v%95RM5dpuu3TFcbTd3fraV(rR2P932(I-Gt#XmO1-k`Cp)m-SQz%^_U({31F8But{ z&erg-Q@R_J0BK`_4ghBemD=H{$uMhKflFnkv3D?^e8g{u&xCTeA+DV|fWQvo`|Ju! zA&Aag1-AH1gc;Yk03gJ36F~`TM?>3nK$El~8^Ni=dN%sp(?=u;{*2Tn^*cUx&UqhA zB7a7$$7z*$>7~cNf=u|^$-Y|y0xx6eiNh8S42}J-szAt1GD%l4!^R?vBG&;Ees+mx zjDG;W*G)a0HWn z{Nh-y)@_3b%6t4fZ=q{)T?swZh2<;xSs7J4tc7Kx2>)-%sCuzymN{$qFP_kq0g z;-&*Kz@yCD=ih}uGrAS|U6mr$Wtl=c#a7~Wn2fQ$XfTD~M!j3J&TvZ$0 z-X=HknznC7HVX5V7L<81$8O(uS+(xv?$+p-{Sx;hihUZlFI5}^LqUVKo!5cuy(2sz zJgK)-n1+8cF7!l76y9JVLpjyy$1fA}^9A%qScQt*5W)$dICoD?Yt)ED+o=*x5v1Vm zI0Px9a)Ik?6@8p@cr@wm1zIC;NQLUsZE%`09wR=GeYE4BO@xzTXqZ`Ajh<=SRW*tH z(kw2`EuMNeUG@iY&m?vk9L7DJVIINcZkR^?AY|D(7!Dr$y114*W(s;{OgpUx>8){? z$J1L{JSLW|`ea;yG>6d?q#m??D<@Tyy^L`tr{vC6;lB1;lpikoy1R1I0#hM2&GKm@qcA*54TRY37<}N$;zyNE(9Hw3f4=*@`5^ZtACe~TnZ1KbqIe0CfT#8 zwfcG#=y8_aW%`S=hlDU#%4^4T7KbI7I0A2o+;lo>`Q7l(YvXeULqeC+73qOH!K`OhY&x`gh+1bG1 zzS>5wk%%E$2`W9SpHz`rAhPLY4Iyc-es9g`?*Ow-AXex?uEHU^fN!gf$ng;LXHz@b zX}`sWHFj?}dc#>#>no+tnw_n)rmcVUKe3e zdhz8fz}QOS-3mwq6<%QsVUF-6Y94%9z)ph1QKD7z-mzw}aE$N5Mf+pv zlmyR8`~|Kb-G6Vn=qsjbcbs5GIuNxkf(+Wu9C2S2cTOXI(5N0p|%kR*_Lszo%JioO4^;s{Kx_H7n3v6N?Cd}Q&Lv~jh3mFA z<*#r01PIY5zDlEU4GbblW}Us-w=f1O3fj`M>}G-3IS^P@Ma}RJ2*rBi8?zHz3=A^l z{Q(g@S%pKcpQ8f&p)hKWL(NAy0e1HF$PLx;p&`3)dQzWLVA;HXcM;hhHeGR)b>%IB zmzy8x+sThmSQv#AK0gO2&!}=bkZz`^d(Y;JVSxdDJfs602o6gg0)!T;k^W;mKy4?J%^Y67ipdpSon zI|x{Tmcw4+`#<1?bMT9YxXy0=oe%-Ll&abnXKFu$0xov$MF!SBG&{!L>_=9&aE*Nm z6y$B~vula@>cGu}Z~(KG;)a`B%g1zFVaB{Mi?EM?JEqduOo@O~H#YKCH6Jw`!g4p2 z{5^gCt6H0e1@faFVM#T36%&QHRCAveKa!Ald*N}u8jV`?kiMz;9F5x z$B{xhx$Jvg)f}of>OE2pyQhZeAq#KAI}~T8FldC#;O3Zc?7{<}cj@%{Yqt8I>n-Nu zy1UEZlFl{G87e*H^sz3;q#-7-^{G|+iyQ7NdOeRyAi**L5SyzKYnM0yxvI+u?e>c#wQKeF&WaU0kF)TtcnafQJ3_IH1*Aq_B(Y7UdT1g>uq?x~bP4 zwH(_Rt@B@NIdT~P^zL8ZQyxg3`GolqTw2W!bQiB&%`zwFX3{8g)4$XUzzL&|Ivcz= zI#y&CN?yIfh`D7}lRBc;vY{_-t$ITZ5wc7g)V5D9BIR~++Mc0ufHVm1fr4oU&1Zze zK611(qcgui04`9IgbK)Jl$7)RDdf#I|%vg+fRL)vi z!18iWJShRO9{jItl_m9m3=aIiW#jyRR(td3E5s&Hx95jWCt+(r--!~?h9oX4tLhd; zruFZ`7y9(22>#^AoRJf!MFAPTsj~)f+|Dj;jUcXR3t~6dFp3DUYwgZSzgg-fipi-JSq**ktFdH4vrpre zR{9_5XC^k@Uw%;tBxOj!a(XS&KE$o7oG2iZ(4)`z#@w4-=n&>>LS1Top;38SJu9da z-wF!@NY3j&GWsT)6?iMX@jZ)u8_yrA|BFI>YPmIW#aJchrSV5iAm9(ayZevw_O9v^(F)+kUmZG}+!t-@c0= z>s1>?iFxC+>d_dsx11U9ue(ZhmC(ov(#l#hK7iacQtIfU$@4)hR(8MqAC%yF&(L}` zj?%}w9o6@TXmt9v(c8FBgzWqHMzifZ2zBse0{BQ55Kq0u4!p%h>!1()>TP;nco#wB z>(A_YZ4Q_p;f#(NK6;5{`iuZ_XU4ygfJ@;FmzLdMfn0Z6EMjG_Y z+asp&dXPLyw^t6OQQgG09AJXD7Kg*39MBVQN@f8DXV_P|S3V`uiiURXI_1{Bordl)S^(~fm5Zf4qD7OE}K2ShSC6$(rP`#G(%n=#i>zQw zVhl)tyuh~rABbhyhBWw0NYB-C+$i(qMHALa$I6^K`fBb)fHXGjNK%)pbF3N8CydzY zo@^Nh8p`CtOEMZS;J))FpAIc(h@gFIbWBtw0|S$Vf%li=uAv$#<&rWz53>y5whQp{ z6~Y8rVvG8n9eaqP@aME2TA$bt7+0Vy#?^9xg#+Yw-BW~l28pmA7w@kgyjYK7(u>RC z4Wb!FsIGP}Fu#g7n+kBI)2yy#0dcL2 z%IRu}@aih7vMxKk3+r((Vm&J> z$)bo%-$~!L^PmbN$pB3`{J>7(5S1|0P&{nU$}JACTPDdCXUk^Y92t+@WLw;zC3trS zz9CoL`^WqzPH6SXg>5Xe_wOCR3h|wF;wHhq|A?1Hu_V_1LuRZbX>Vj^1n)vB(V+1^%H>lUB(3!#EY^0yQoQq?)Yw+$xt%h%~TFjfl+1l8>iJ~0b&0&O2K9^Jcl6T^Ub zOssDaT!jalnCW+RffxSFzj?oSqMWWWL%@lkWno(BD3KviDO?^mML4B;MdG<*<;Zlx z{!4z4(x8RHfzDK`{kUZ;n(bLI`Ki133b}!hMV~wJ?0fyHcrq4cW-r9>#i$|Xqy1JG z0OvkksgCRKR>JvY%@&p)fX6h&Yv_l5ch0b4>_^q;*{GhPA=fUQHG6B6-XmL0Z&Eo| zkOH|yR4s2^sOmPFp8OP5TLup|^)pvs?3vE7uSfVKMhqjZB0lUIE%~9mS(fEQhb>@J zs*_!fAhL5Qt8U0y7~tRZxfoSVPEqCl=yrgl%m&YAyQy`n7px)FoWswgmphJnOdXNg zU{6Y*3PAMn@e3S_{Tm^m zoZ0fE3n@zs79V~ZQo5U!#<1}As{XzQ%IJaaM_G^LTW#j2yu$_l>B2qzQ(5(L9#qw~ z))Rp(`sWlpx*`1`bdxnE_aHA$>z>$zu6*5($Ouzu+R@J1wQ;&!(}US}(KxcyZRb_l ziQQyeEj5$2^$`C#;cZ{9(D~-+s=5tY^3s>2^8UDej;7;t>GnP;qfvR#H-%qHP{$RX z#!tL_vmGy5DBk|?ZJs^Z-mq_^TsOz877`VITZw2~a_MaM?8?Ri7Ez+ZmBc7LI{LzM zNP&7Mz=cNxrYW^4v#srJI)lD&T)a4ycO+`Cv)ml|b54-1Jj^e|&Nw1!# zYd@yN*Jsomho68R-}DR5_iuzI5?@WG<#}6%n8Ahb9CteUBaS+HQ&M!Qejht&Bts<+ zo1?5suWQ?2Jnz~5ajCRl3 zPqY9v-T#L&yxIzSpJh04LD>cKQjEly3l`58pthrgMxs1j1?&l)943$ORAfv zXO8o_B2HX%V;RAD!Ry%sx4SC--=pGYd}#aSG0*IO5g z5X$<@)=}#{&m;H5*E}vyy16eyT{R~}(eFGm?3tg^$poizp$Pql<0!VTJU41xhTCxA zHKLH!TLA`(NcNR^ zXM}R#Q>@%_6KKp;g3<}3+_$yiOZl5q1Vxm0UX^41h6%$S)jjOh=EZtV%wC1rZf@k9 z&ND)Vf8Ol`Sz902XZd+o?~NRdL7Y~5Qk4*{Z;yFfeHW98s28ON9$3$~4IP~a0yl3j zNC2)8@sIdI^5*UEdwTUNmK$$0J~TFpL#DRulnRx*^i%uXzJv=&=9R_<7hH92U%ILz z*(y3GsyTDz%mP&R(v0k$IB4VfuE`G3KKe3cNddW4O6@MRGqw9AA2R-#IVnnu%6cqk zx{}XlXsBb>|JWxBe)eRR%I$#TB*O_2%g5!&Shd7R#da(hrZA*cKCem+i$GBH&h1rs zbTwu=>Yg#~g6o(1t|C)aVuRuF20lg_z9(~vfb~5 zNprk|YD_`gS`xz~UyGGUJTU#dc8wI5)0jf8a&S1gAdMb0#I5dg%IogIsJnIfhnKL1 zhs?$!4F895iFNDd!&1U8T@b@1G0gkp7uEy0`mQd&_^^T1mjUC_?U;DVTedP+>}ST* z?=@4{#Ljm&7QlKl!~gsB^8F3h!&i@e-itp6+D!41OINHmg`v$7cMKqzTbd1cR-z<~ z<1RjHzOwS*m7CjUVm_{aTh1h5LSjPA2?m;r6k0jhrNjyg*j`4vK^St|w5e{Me2M%X zM9ULXkL>M?cU}sm76Q{1@uNvJ#9vEeiZ2-^V0rVq7d1a(#XkkD$5{#a*Phztga+uA31*KC0g2^&ku%MyFZV6+-5A3U}o?-MF?g`ynFxko1o`3r}q4i zrr7$A4M-8W{IzuZ={e*zRQ)20;h0aJ#;yLE?N}SPbo&zu{s|QJ-oD)L_BnE)hAV7K zjT9Jsa-CkT0;8v|HiRQSG2om-1&FoxZJws9+2*8=(W#}}?Gf~eHzpgAMK zLKtQuvLa6Gugzb!uN$-acr#TmbgYqqK)l-Jd1*{(H{M<$Q zxjCf_fAQLO+Jp6p%L_prxO2L7eAF2XfI0LV#_6F~p)NcDJb^sHJyteM?ICfo%oHIi zr87_;+5jAs`ZO4)RFOaRjxZ-5&=vG-D83O&f9fSZall=&9y&w-_A5ly<tM?Tqq0T zLaE$Sk3O&gecirQJj$?cji^r{KwlxxKJWHm*I6Qd=A~GOQB-no-J0asGoS1OHSJVJ zsJ47JYB-z~?cl$JZyG3cFh)^G&nK5s_igT;x$`Aaj*W}=#`nL)@BEcYrtNx4***1W z&!gRzf}bDXp-^)kKKXtZTP)DKs>YKRqibKkUg{WovAT)D@nPu>U7c0+*M01$sIU0$ zYdtJDa2^2{s$S;m=X2sS6i(^{zCV#mQ9ja!4hnzp&8uAS{z1FC7&%l)$sR-Op(@ym_5?eF-0>dFSc?U z%v>o>krPOJh3K#Vp6~Zk+fp@7gK&Sx)Co&@m;IWaNBmQi#CTP_Ml&MEzwr@-x7b2+ z=QX)&C+DPDtE7uLwcp$LmDQcy@8`vhpt-)`;zf3iHr=h?_q~sBKcCAY#udF-|mn&hlrqFXfas${wEI zA3XJyeT0yc?#6y0dZiX=VT?a;vYo>db&`jB4O<5L)^Z*im`)~+s=U=5~@sw z@PB{?;a{CQtJR+Hnr!XydH|Kuhji_l*kgHcs$)`HR5KEzp(mkekpjybe!BDoZGEs0 z_VDaebJDXWx2rMt$b;L<%}{Vl1)^2KRzqqW@B9qT^ipCrBvWs0Xsj|bk570qEOBI# zlQRN#s_L>h#aLJVcVq}J`Zh$>U2=(ob;eLltK5eUS z^ECfe99I^fu6#@LM*TKDH3C`KD(0y3Js)vJ(~9GSJn@qRGGZ~UUm>FV5+1*B%hXBJ zbjm+S(S*=FZ@&B&0OPiQ7a{5M#*disTeSh!6&}NuZYK{TrShR~DVJI^zG#J&LiFk1 zZY#FJl=WSODbZ%Cmcl0yF}0Z;`5tWvRDasNA8Xvd;=U7yF@-O=(&>+do4?)a2g_vs zjn{>~t$o)Lk(J-qiOPS*r==5vR{euQR{s8I8H3)Nl>fqyLmJe6G>U_P#!mkQ(I~gV z3|^nFG=;KK2F$|!^xFJ>`IR8kCV)f55hEybSfMYE4ouGzV*~&59XCw1sS+d28-T&} zIF|bBFS}cBKBc&1ETj9v!F{cB*e<4)!(l*tu*nE*uF`Cj{R^YnwE?}K6)n(u1@N@s zBpfi{?VQF7{Mv(#AWL@T9iuiw9Z+WBuEmk&}(f19X${f~bK5BY!p`@ijF z{-;mIEz(qdI(oo6+}~c=@k=MMtb=`Qj;F1nQyJba2PIaI%f8@@JLjRF-vZs3z~@Sm z*U;#4#u%9`Gj09a)Z(K!(30T&>A8Igh2sk(X8YPX1a|J)ZPNz;F7OI$-j))?6Y|Y+=y$005 z3IVLA&Pqq6pIK34lhF3pi*D7_+SITA5$h@22k2|p>|oOf&16I;sNy#h0WU9~y+Mc| z-8=+}Fzv@2ASeI+ctuw=wC|*dL1VqdIB=@Q5J4^KWHD&SXB9~4qG$~6{`rXa;Ptwo zg<^mkGO6rw6yRGIg@CTP1*eLuYaMYqr|lR4R6r4rXLn&u4#9-oNLzXLSlL#2k%xj5 zJ%?93_s2B@*CR8Yr)@%Yj`SbHYfp^Eh`3QeK5J!UsVY~~IC8K*;G zHx!!Ywa3Tt7`|BK??)qiJ_G-WFnP>jT!2g`7NV~M3(e7U9prjSBVZh%=Y<1QE}vkf zpu>{5jk!Z&=_%+97wWK!ej)IC$#Z1tOf|0sB&PV@oY)XDlP?^Hx#$P>nfPlCQul39 z(Ftz!(vafG6(gx8GoDbQW=L3XS*2~sK_0l5 zzo*vum%EfQWRKx1NUe^^DEo%vPbm7aVLi&T9w!BI1t&I^xt&zhKTbor{YB|Tk!n@^cY3jyv&aTvs;blG8(ikFkWRkc__1aO%` zBN;q96abW8bNeTsd%q4^^%w)Y>~tSCNOudyL%oBKxNQRHDwx`%&ph%nth?2%Gu7Wc zaQ1urd8(mkLi{*$P2vi3d)v=9A6?F`jW;lRUFS};oAO93WHOhZU zuFej$SKnOSTIp8N*AuIu&0bG%Q+K+}qlWVuug|g5LtePcI5Ob@a;ynDZ_N^;N<;zfq;|#b z>3axJOVe;3kr_9GTkET57m=7-q-U(z{g$WVtjGV}gC$iTZLOuO==%y$*EK?cYkrRC7>JZAHP_@w8Pi8F^$($z4`C2@KqH%Cb7hNCj z;6Xu}IHfgH%iTfH(zwViO@bRfYaCne=_z+b`7QFOS)&Dv_t8dQz>eT$Mn6p}>C4xWo#>df%%+ zS~E_1UbL(3p$u4U4m66t|A?zXr?meGncHa|J0yVR>D$s{`dG{CKlZuf`!GUhg+gYs z-y1y;kUDpV4m9P7+{SupLVJW-23uh*{<<4Fk@}Y&V)#~@o;k-F4W)WZ@FB8I1z+vc z!^RN<;B5HM5AgB}n*yAR#wk5oQKId19Q& zSqvML$CzdB|5)w(1b&~?f~SI?3{y9?`xz#7^8)=$_(8YWz>C`InfmnWs&~c6FB3rgZ6RU0WBtn0g)}6LM|YyARWi z;dgCb=UIslZf|i37i)5LMQTnw)vc=eEc|D;!fx6E^Vou>7bLO$4faWN!oDzn|0o*Q z9p8Fcyf{x&%pO8RX$)iF;!KE|S&4~Q+UTtlPbi|f-y<4k5>*eLaCb|4d;IUC@ZQmhFv(ZW z!cwYz$V9z*eKoTmS4x-|P0ikbqcH)PR@mOJLY+e2g-$3rnSzxiZpzzYN}~X?tq&Ix z<0oQ1JD=RI7lFMvpmlbkf(&l4A!u4UQ$w4hKq; z##u8>=|ZPBQ*Ppscs=k4^qlzoI0_V!Gj97hSDOSDPV0((Vy^aT3|P{21o{)dolj86c)iS z@MZb|YJTut`*p_ZnRgE9Q>Z$zl(~TFJFTPZQIrha&6McHuic(*Kz2$%QObNDM>9Nz zS4?YRe`c(9QY3I@iGQDfV9Rv*tkqZgxm?M(Lg%)@1=|pZs8>7)o_9R&>x-i)Q6IH3 zz?qctAux*436*G`2ki%K&asTNZwbKEOx7FxriuI0l*-bUl(>l}Ccz19v?VmbC+mA^ znfY;|Xy4DP$!{W{NWDA4(;i@R2vZu^!@S1>uX6KreGF$pOmL;FFE3HK>7pjBFnc8{ z^*cdPjmG?xrmZiYZXuBWyob(UiKWd+M;$Q@hBR|N?Cjl%D`8V~Dqp+%)z9*FN~Up_k2y_Nvo zn+ykxrb^mw9$>At&H@b-HJ>bW>UT4aPiP64=7>iT=IbC9kQuy-7}Lpq2{4e-kTP=z zF^}U%5`h%*F(dpspxeh@!vjphfBBPk&|(;Ltom7*;sAe}%;@p;8LcG}4^89|%PzRf zjAT2#(Y^m(ACd<*M#OBj%*HIgvcp(KeT7{$5%J&paA63lmYYNWSyR&*)Gr%uV6Nau z(qg&X&)sO4#U9VmNNRyyHH$r@dXsyhy(U-O*HIRw1irI>rs zI) zwHCX!D`Z#YsR~(7Ynv<*_PEKp0|MyT>gi-mxFG9XDMADmi>n41MJA@$kShZ@g^VMvJ8Q z+YGY`f33eaFc8Z(oHfkK_j`kdFN|Odtn953dcG3cHd$NQms-C@w0I*wPrUmSwMtiM z)eTkLCb(dv**9LUnN2t9zIpgRRu9O-CB%<(h>~K)w?}-E$1h&`9H89U78l+}*wqRQ zVXQreO7cTMW8l6Gdf|}!1GH~)xf|W9Pyz>wDYP_WF*$h4+|=iYHPDSSWiFHp&%5#;2n@K2z({jgqv`8o zFL@hzm*5W_dZ$UhE}C}VQ5Zw94ASeI?f?+Y1F5$z&+Ec!e*MkTQyv7Z-_(;{9y9pK z&zy(YQ6$We&X66iiJ`*cIrNuW)iGiVGO z?U8VcV7N4T?FDsYht~=zrU%0nUU9zZ%gBi^0rV#8$pC{YQV=a+pV+XOK?Fj%qO34K zSX=1vj^_so3j#?h*8syY+;_|!PHAFE|) zsQ9*t1?L9+!1t2!f%6OjKBiJ9#KvIFl#4GG>lbH(!^T3J+OIUH*M4nuXNTpUdl{;(Ta+i23R{cJMdQ3!SzfzH~2XO0V4NbUi*ykTv^$uo=0FtlEVwWzcc5Qm#gc-D44pK^pHei2cdz_@M0eHjT-gNv5x&vGr_F9k z+?jOCWmVfWZp@wkxdHlh80$#%{rNdGb^I>Ak>w^PC5&6i+BmEpSpu|AR&~K-gGcHe z)xsBsqT?VV*gcVeY_)NpGNamig$Ym+8^E^Cu4NFR!ShO1J4_X-e;^74prf{+c|scP z908MQ1^l;)%R#Cf`z=II94G*3=d}$J@67c&EtD{TtraVR3QY@tbk+r9vf$`D`B+4( zKAJWRcyWQC#aSiRdt^1F!f(3ZT5@YNv~aK(lq;FQ{3Qo2q0MlrjlUp52;@-K`h4XS zq~8p)LGNYn=+cZnWp{f6<~Gt#=bhx7-Rr9MX{uz*HNEzBUzk{12UzqU{6)R| zd>Zo)5uU7Ebr^Z>mayU%Oj0mu2VGqfm%qQ09v}`V>}T5Aw;|!re!es`|TWjusCm|M5y zLW_UoB_vI9UHZqd6ZL?iY$0-XM@9`(?i7iCC;!%Lxa-!D%q0Z1T%m8{J9|E1Wv_kj z7bTZUuiHHBaOMJKM3v9=RQr)>nT@HT`N!W8J}OALcHEN)>uv>MwL@43sI1A8ec-Vm z#WDUKNYyP7Z6*gsB5V5;ZlJ=K-{imkUWh&~r`quV3SR)qp^OwVhxe3PAHq>O_Udke&9oq3+`8L@ zmpy~ra!0XkhE;ARE?(s|E9!;_9r0cM*%ntrtEI-PMcm@m;zs7$)K@;a z$CSz$$lGvtoBY;%l#gh8z|>d50tt<;xT&D3dUS{vAr-Z5UTML^WWZp=4i6EhoK2r1 z2yN(f(C{uopyEGSuH4#@=eczv6=pJA)bG&QEJS6i(#C{9O71Hm6MROWj!o|C~8iMh zsZ2OOe({GL%Ar36(wyfrBN4ZhS(1D^EBYy|_FUhuAs`fmw{opYFpR@T>tow+(beyo zlzUJMMAUF2yM8;-3&8@GvGIt(pIqD*EgQ$V6W*>-Qz2>N2!ie)Ke7->mK<~+sEm* zM`z!Snya+JGCTjllz3*|2#zP?KG)BAFmCylv)mPjRp7Tc*iFj`~ z8>WOG&2TxUv1KeneKRXruwJla0BjUjgqj3z@2jQm7$mLyNWA(gM6p0!5B@2`?et-t z4Gzbrr?p>Jca`rYg{yQwM*r$z%CNr{vc?+CuDB+l+sX*7-4@A{1J<=+HrDa^tB~q6 z3DkZJwjN+q+`H&xHWv}H~>z*@U+#dCil1y zb%KB#q`2c^+kAHV#;Cq9rBS5qEykC%Ho~jA|AYiQab=D9@fi!t44unZt3618Sf8}b zhYP9-{QM%kU>i}aXRk7M&tL0zvHV0U{i&gju+Bj#$v(rg-63`{YTKkvH6PSDlI@lr zV;bv|CHd+CdB6&rW8M=L&f`w?5s%7`Du^oN2{+D+Co5r3=Sg3=2gTa0dFPMEJeR%Z z{CtnzNz39to>KZ>#82@=#^@?Md69F$r|12Rbd+rs#N?ZfCZX9iy#CG;N|E}IK(3DV56Mq#~z;-3cMCsK(09|X?1sAu=e9qME5)xvfp zkyRta`i}c%ga}JSgt?^!L+}s|Vb%uY`XKy{j__h5Gu=V4pJ_;{)fz30d;R-K<;4w> zD+GL4SFMV3k0TDiBD%X_X!rOYGOUJD7U+(D6T)BQz(23;%oF}UJ}2Pc-GG#mle+2{ zioE*`JAE{R;5`G6jefS0cKfsdVXMxWE%q#(#Ib;#M80Xh1F6nYSI@Cyscj+{Y;MK7 zg&}Wv!J7?jnB?It0r2}NT45hg6`e{_#h>v`*-&07TS=>F;E(73NT+V`Zzc47lSE$m zBYH8&k_-+o?XUgkb2OgV!=E|!>whFnx38t6XGSR@n2bTC-F|f1qspIF+cu0IWUaA6 z9Nu{gV%oyy8m2s-@%54{$v%^j>5l#AZ;&Qt%^%+bo)^}m37$BzjeL@(f^8LTTOa+i zf+infQl~$s%s@7MlKw9S-z8y;Y9g;w9(43|^tG~>+X+9;Aw8^XR%&SbrFi^#908G} znCH&ei~|ZlG50(~{Kteo0!+jA@Jo(Vs4~v%g3+_Dc;kptz*`rS&UJ)R-^MPbn-B6l z?0>O#>5XCPu=C<5%uVAZz$g0ZKJPyh^L%Ea()d$L=+TccjJ|N=EiGl?6IxQInIG(c z7l@}#`kPPbglz#k6M7VyIvw;`M5itX{Sr*TxkSp7Lxbf^K7n&ijMO*dva3Y!%*N8v zW^Sjg$RfKYu-nSvEVVprzLi8f9$UL*$^N8t+s)FuPPM-M=QDD}SGRDp-^69Q`Edf{ zYIuEgEk&{)pgZGMKWnF~gjGks*YaP$bt5(}v@vU`g6)R|-a4tbAwAtD!zAdix2y-> z<+lW{g2u%MU}En@VQP4i&D6WE8%H0fpY#oXGVJL*TeY}^S zf8M6fKGz<8BEq55q+4aE+Sh-_O7<UaNY&BVf0WSSf^js{FKPXVAPvf9d|?1o$xp$3n6Mqn4~axNQF!mgk^fimKl7 z$R2SZM`MamkFWTuk|E($d|AZz;zJRhF-A~qkn<+pDX|e> zDU4nXBT6_qN=jt1{H}>nb^HJ0;95zmMi)d9_$yllj z14@Wix2z*YY9^a~S{yb@EeR@?neP14!y{q+iGe}M)%MCr=O%Y{o6vbg$ax5(uWwpe z!b93De5rE8MaO7G)L|5m^=a4K&D1+VxD;z(J*;|cQ+t7mL#c{6q^9ORr<_(5 zfI2SC|N2*Oc>TZUZg{(Y%QHHQt1K<(1`TN5`3{dx0L|e1N=60rhNt3*#}+CC>f&)S zLOscQ5ReY?0eQ$u2Aslr01R;suz1fSx;>13cR4;*NY?rL?rU;5?`=y#KPtc$F*oJb z$zej$T|s-!b8AOI;yeXI0gz0D>%#?$P%z(G!-VZCpGv*jno`kuOGxBZZb50L3rYth zP;!CtUN&H3GlHnj*Nq)pt7rT{>p?y7*8={5Km=z7yji!*5B_&E=kdtRQpk zkKuqSp>3Us-j#8;C&AhNl;L!38|DSOxXLgq2y}V0a<|B~&_M%&j^>t))<0f1r^Ub? z(Whq`uk}oVCYx|2GXcjm=KCms9F#r}l+q{@^qxs~%iNkG^0$0kf1u0B!@8?yOLiXg z;R7~b2sb65av$OA)$fH_Z7^bf&qI1W9oYv;r1qAxrD_pf` zv|B;~D3_VJ^^wWaVxlKpBN0(}kb@aPlJkq(3jxFSwH}yb>ykCUP(JwzCtUw-L`EC^ zMth%SzTdgI3gBBoVN}lmfY3x%vv&*`qz$e4P96c7zFUvDQF*P6`%XN@9Qe9uA~F!b zuLD)8whkfF85H6+`>EE>aOiJ%I5ozwNFh4Y%`w3E)!XI@Mhgxj%``CAKXN=pWHRdg zKnb<45?p&ga;4M2J|_Z)uHEb7Vp1}!0~0;_IQ{n@HhU-hjV!D;NP;}t?42%>U&o;u z2tP%VoRCRJsye)01*1w;(op2Aj&)Q@yz9uv|FzqE6}t)FReEZhC-lGcNLc@ zN+|wZbx1d@A4qmJ(EV*45a#cK87cIT+3<}rIiQrUU-m@nXGCUV1WI&NfD)b7X$_gq zwXJ$s6wT0RZWb`+y7{18PiRx=9TCVos4Q9N;;s`D@W0#k@}_6Lz6qoXqWy2r8cp>K zk=L8jfslI>-n_D4%?k7=Zhr-K*w>y8)fOI{e>Wv?iE}6>z}Y?Wme1^ z7$^Xo+RQsSh$z{Do?IA&IhS5cvU#x0=qz{K(l#J3ajgJs)sy9qlEcP>2iS)Y;0%K) zm$rthgM!Zt*o`_#zFo3^cZ_~fvgsliE5VxzZ_EYqL1lpCdSx>S@s|(R2y{K^l=Ak> z5ojJy0DZIE3NQPXjE27(pZ6f8xOf+H=Cm$wo?L+!1u;k7$&eEg4v=H!VT%tcT8N#(m zL)qq^?|Z|!tR6}2u8or1W~<@WvVAPG+9hUh&PFINue+QXeH9dyx0&bJX>TN1^=Vx?F403nL(hgT&Cx# zw59&&Bd%c+0j^!Cp7+0yMd*s5bou5)K7L@10Z zuI=y}1rDwcfZ}+!p z{lfa>%JKwCi8=P={HY;%rNEUm7tkh@uNyZmKz3<%6|8%8)}}{w_@L3uSpk^K-eKqB zINLSaea-v0wc9D~8jIOXErAo5Tl-U)=;_w*TG7;Q~7|_K*r*pED4Dt((V3EDy z2ngrZHd`_E>@KhGg(Ktkg;ikLg+(`9(bOPh!F6D?p#zOMgh~{<9cE|Y^a4I6)cDk} zv)_n)c|ffpkx>b-->Y9!O5vzfgKC>NjVp4 zfJ(=rQG2?>>5+^tZZm%VT$*1b<#S1ren3Bw8)DmC2@VW!%L_-LD=C!#9ma2f_m`fX z^u*zck$4lX0SS>IE&ZUhqvy`NoZeI3=p2{`i=OMf0GsYfxxNdwZO=NALP2mWs~N>; zwro6)8uB^s=-uv>>KFp+Y!#P}AAXx{8~QUy$7j%a!dr#(UPU>X=!lAFRWr_NADu>4 zPydj

N@c^?Is;RHv2c5J1|$MGU@)n}u#-2}lg52~6swC70Z82{}u&*C_KII0Bei z-WaYOyX$29zLS$>0ulu;6?dv~43~jEdJZ6z(KD}`Z?9Y@#cfC*K&~$@h=0KH`{hXV z#PO_cOAF>`;>53@E&=XM2n|(IX4e;!^qLqK)c>%5fccGYhVcSJ*WRBXuMvKxev4+{ zKK1!gtM&a4EQ#^@@@50Or|>|?A(2oGzV6F4%f))cH<}EJLuP+E6%7p-1iD6s0f|55 zmG1?sBVs&5!}|E4FU_T_={3GOyS3M@R+Y1hMr}GJmIRtl$&3f~u6OowTd~Vg{IeBo zg=B*hsoZP6gciIE>>0dRINYb5bOKAB1Nu>B`eQRe3_#s|)*VFE| zUhs1FIfC&?oJv6-(=DaR7p!DyjQsS|>qZTIywWaC(~t2yQqdKLHb>=XYumerL%Czn zNTLd*BbVRN_=QsXL*v%<>4ZeN4s7DyzM@w0**{HCj3?Fx6qA2%Hs~;sEyVcjeCps+ zZImSzEOw^o!ZbOR5pT>Hi0i>qHn@QO86NQSFWVFypdXLF zmPG8flY8cN1Bot2(_&ol2(F#j?@wC4;#sPt8Y_~^lOOUQ-*$Op&gJ-vLx@d6@WbS; zQ2~(7E(4;jJ&o>s_Xk#z2fvse|J0mvh<)6##r_E_7WyPqx2i*T0BVZ(>}%dptW=Rz z8KwF}U2KXFHKsSHr z*PA==6W(*QpO-z*@z=^hWT=Wm78A{;Wps%kqG)$(XgexyB%yz-I^FwPN;Ao1f!Vo# z#y`E6_8LZS$4%1MNzpFUl(FFHfpAu{?oL2zgFV>-fvMu=u#(X<*)}|I7@hQtk~VAD7?aIrF(YB zIg^KnA8t=GQ4)8R=R{PkvAo3JA?>mKcI?OZ*(M#220dDm60Hm+b+=o?j+RKq)vI?$ zRLLKlwB_q;y=BYUv^>Kf5RCgRGT^K4X~DabGQFIc(c1kTjv~gDU>7sf5tcu0Q0a*+ z2O*dzEmQ_aa8b9}y zTf{0A;L70-0WsQ&d2p@R8Y!}N#)so>hktM+XabI>&O1N6E~Ql+(+*~6#3R)Px5u^@ zKZL;X>?VTeXQ&MZScl!2P~&W>)XGS?p4bK0!Op zD$ft)wQcRlur>M-rj8BfT#Zn=w8|sbN`?%V9ck!^pm>C5yY!E8S(c6%PU+eZi`P&u z|6ZROZEa{Ne4zD@{e`HCs*$?9BU1@&CS&E~1cA=ny!WtTv)puwM8t^~4SOzRD(t}T zP?gKx@$e$%21dqDA)XH1DMu9#CqSJNMyFjd&`rBwBvATp++a}J%iHjxHc{|FgO6RbVALnyJ0NA-JN zZ0(VFa8KR!!|Ivv9hxrvS3b%+YU~xhc#hjULQ-!?C_W>r6W8!XImx@txB}1&X)eat z`bN8W;w?ryVKwKO|2>p|5-Let4SyIOLUTUgE%g(z{FVXbSlJuP0`sSY zE}rKv#Yv-#|1d8EyxUze(n#(FhGNRyGx6D70ffo0-}Pv6D;B0%;79r2C(*P`FQjFvGFDK$k6-a*6z5rSi@aNzEW~yd8IEW z%^vbVD{B|}O!4Bn zL&)MCYO=?VOKfVdpAxNYJ5G2kVP3!I(npI1G3$8II^&0uX?sDp<<>0_Xw;7T6sN$8 z=I2|2ZtG4egHhh7;%^gKa&R)kFJQ_kngdd6T{8Y5W9(b6<*KWKyO zA;XQ6vi6ma=K7X%w#nx+-&Z}qB;W862TxV`^}w%AEr^yF=yi;zoa_j8vuAxO1EFb; zkgq*DDx5;2N*H9n91^7yI1kGkogatM$PU4p|0D*C+iQ6ctYj9r)W{1W{R3-x&=Vue zA%`>if@x`>FUoOiiBE1?{JtDZ-;R?x7-XXLzJh5JS)^v%c%*WH8Mi|ADT(M=#jC;T z&Up`l+KEge6YcwW=Eh4Qf6l@5plEGCwW9rQoMPY>X8#|J>@TloTUY*feaG zEZFX^ydJCcR3kE3E4rldIYtPSL|Gh~ybQWM-OR^Y(`Azm(3dXdSjAFA$70(s8d1wvQ6#j5QukY9IaT_7u@ldVkrnnG@j>FH1d_U`;8Y?G z`4$~MHcH%5ZNQ&dcS~m#@fsPa`c|(fCK1J5|2NRU=(5HY`=pO|GHhc;GjfJHsa`@<{sn1fNMhCSwv8#LsRZt zp1~pr`BUMFat)`k7HKzrdG%P>+>P>unNoZOXGTXE*3^AR{atMEE`#sS;C@JR)#a;_e za4dayvK@5NFhvy(sHvUf_l;)`kggX4M7F87{MCN4zXL*Jj4m;yR_Le}#M!l@XRw^pAVHM$m64!HCc%{>*Fkq`&ve9iiYc1y20vG z|IVB3G``u)sg4@Fyl(=~5&X@xUyR@K_{-G#f_n%p;m7f2bA&!iSKsje0O9jY4w@LS z1*Z`IKMA%Mvar0y2}L8_G?&FP%w|F9ZFB~f`zzj~gA-6m<^F?s`>bL2Z6=U35|JU# zGiA4j0nTiz&TwcJ7u0A@Q-PRYa%Lg(cYvfCpMsfOGf#coAjFSJAPv>1w)yZb46up- zx7Ux!5SnLthIxR>&-uSVD#p0}zqIt%)3b&-iDrm&wuYjw&=Lod;xTH;r;Qs-pHqY8 z*}-5476zBA1hi854WuEV2dp<8?W)td=f=n1 z2Ey3ifSuAxmS1}6O%!-)^{n{C+7OJoHN70Rzybw@tc`%F{7VB;X}Q_@tL)OFL+VLJ z)n>VT`g{{`Q3=TDEc6RRdO_gPqeWptxpqZHP655o;ZgOi_LXQ1ah)G3^%Y6Eu*|IwPg! z`OD6BAixp2c(d9fKKa1>cuam0XdTc@Y)(L&dFR$j=mdIgd+*Vite~4I`{k3|(m7O= zaUGB;z~xp1Gi*VgA>GB{mp(3L@g|JdUedvy0~M7MDZ!%wDk9eHzq)BWVvm6}7ssbP z!O&jf`FwULziMJ?sI?6?Vci>R062!AJzpzAB?d*JJzBg_vfX;px)we1FO2J*JrsVz zwE{z9kVU8Aswy*FkpK8ZqZdxRna}{0M)S-3X98&WTYit;Qj&xxgIQoU`THIwqs2XY|bcM1qpzLqW%XJ(72_DehG-9!8aT zu?cM%jjw=3WVz__eotEDItIh7-+|go8S=+Jt?ydk@oNUz2msx@a>F{Dqx6>Kc2{!y zu8y~jbOOn;uIZG=UTjFK!P`RCSQ^SBC<%!El$MeU*lhz%8dZRWh@nvbF$Gx%pKzT* zxk;PnCcCB`L5XJzC}wubFQ{adVkp=DbLZg+YiyzHT(qz41t3ZRy-eyW0fwS?dvg4L z;*v&u=JQU=P}T#?RB&lpQ>@|N>`MLV5Qv6;rrU2>bfM&;USvhZ#{Ea9<`0L4=-=y& zI;=P~D$y?Knx2>M%*$0}A2`@~1nV>j=bqmMyEVMb7^| zzI#C_>7cr=?EC}&y2P@7-HT<*{(rO!_BR5vzFx-l@?~q6p?POejY>tDunU3%!@l|Q z`h%-30C7|O=ubdv7zl`{Mvlr*%6<3sbi#GnB&=_<+xihAeKEj zutxATmK{+P4Ai|JA=28W*FunUslHI$Z>!~9nFUhVd*)xVKH0tZYiSyCAm`u!cnSEL zo?PtwGdC}8Zr1M(=&RAdW2n=ssxb@Urc^E=06$q8?&dB-8kSB8TD;9osVJhM!JUQJ z^dW55|7uL?@~NvUiqFYOcD-_?$Y5^J{&}*)*n_I!viDmV9mC%qEL~VtuQbEV%#0z# zIRc?v-SsUoERTrxjhxz%M@lIG_fzwoYuS)arXAUvxart?_L;v)-z z^~KxFEU3FM{m>^3GV+`0;)74V`4K|kUu0$%CpKA}R6{(3s@QOSRmIZ9qA*@1BQaI7 z26JZPw93Cuo^)e{5{W&Lx|a%qcv?#%alLe@RICjLRAm(#*ppZlXeg(36INW2cV0G> zV-n5ozF#uI>=9iE>SM7NCNZw1|Km*(|?G-_L{p$&CzSBPZ0g$X!CSYXtH#RnoJh9}R0Rjtd(Q3t;{JxRx zq8EqnW@b+Dtffln`i*+(qFCMobH6{_%4%Rzx3PxM9^*BIp?vXZU+q8N)6~?Y^@ABL z4dXQOESQGD=&twlkL#-T#P8!~*;fR;E9bE$8#x??6vKZ1Xddy+_62vi*^Ic?br|_e1`CR1r`TMkux@Y*1dJx8o`Z9_id#~Ut5R?Vc+N?7XcUT26HUU zZ(z;~&HFktk?TF%n#yM-G37^w!mEf97pQk$&pU~yyS25x)O2}KFay#6TK+aXlCNU% z10FmGZv-3538)KoG%<8fnZMyTi2w~Xw+c+>;-GHSb#>8=4RFU|V|1X8c!DYT{x)!~5;h5NJwW_><1;^f62@Q4(*%L* z2w4YoDGvln-j#Jf=GOpciDOg@-C=$K;C%j^$& str: + + """ + Get the chat data need for the gradio app + + :param question: + The question being asked in the chat app. + :type question: str + :param history: + A list of the conversation questions and answers. + :type history: list + :return: + The answer from the current question. + """ + + result = conversation_chain.invoke({"question": question}) + answer = result['answer'] + + # include source documents if they exist + # grab the first one as that should be related to the answer + source_doc = "" + if result.get('source_documents'): + source_doc = result['source_documents'][0] + + response = f"{answer}\n\n**Source:**\n{source_doc.metadata.get('source', 'Source')}" \ + if source_doc \ + else answer + return response + + +def main(): + + gr.ChatInterface(chat, type="messages").launch(inbrowser=True) + + +if __name__ == '__main__': + + create_new_db = False if Path('vector_db').exists() else True + + if create_new_db: + folders = Path('knowledge_base').glob('*') + chunks = get_chunks(folders=folders) + vector_store = create_vector_db(chunks=chunks, db_name=Rag.DB_NAME.value, embeddings=Rag.EMBED_MODEL.value) + else: + client = get_local_vector_db(path='../rag_chat_example/vector_db') + vector_store = Chroma(client=client, embedding_function=Rag.EMBED_MODEL.value) + + conversation_chain = get_conversation_chain(vectorstore=vector_store) + + main() + + + diff --git a/week5/community-contributions/rag_chat_example/utils.py b/week5/community-contributions/rag_chat_example/utils.py new file mode 100644 index 0000000..5ce8123 --- /dev/null +++ b/week5/community-contributions/rag_chat_example/utils.py @@ -0,0 +1,267 @@ +from chromadb import PersistentClient +from dotenv import load_dotenv +from enum import Enum + +import plotly.graph_objects as go +from langchain.document_loaders import DirectoryLoader, TextLoader +from langchain.text_splitter import CharacterTextSplitter +from langchain.schema import Document +from langchain_openai import OpenAIEmbeddings, ChatOpenAI +from langchain_chroma import Chroma +from langchain.memory import ConversationBufferMemory +from langchain.chains import ConversationalRetrievalChain +import numpy as np +import os +from pathlib import Path +from sklearn.manifold import TSNE +from typing import Any, List, Tuple, Generator + +cur_path = Path(__file__) +env_path = cur_path.parent.parent.parent.parent / '.env' +assert env_path.exists(), f"Please add an .env to the root project path" + +load_dotenv(dotenv_path=env_path) + + +class Rag(Enum): + + GPT_MODEL = "gpt-4o-mini" + HUG_MODEL = "sentence-transformers/all-MiniLM-L6-v2" + EMBED_MODEL = OpenAIEmbeddings() + DB_NAME = "vector_db" + + +def add_metadata(doc: Document, doc_type: str) -> Document: + """ + Add metadata to a Document object. + + :param doc: The Document object to add metadata to. + :type doc: Document + :param doc_type: The type of document to be added as metadata. + :type doc_type: str + :return: The Document object with added metadata. + :rtype: Document + """ + doc.metadata["doc_type"] = doc_type + return doc + + +def get_chunks(folders: Generator[Path, None, None], file_ext='.txt') -> List[Document]: + """ + Load documents from specified folders, add metadata, and split them into chunks. + + :param folders: List of folder paths containing documents. + :type folders: List[str] + :param file_ext: + The file extension to get from a local knowledge base (e.g. '.txt') + :type file_ext: str + :return: List of document chunks. + :rtype: List[Document] + """ + text_loader_kwargs = {'encoding': 'utf-8'} + documents = [] + for folder in folders: + doc_type = os.path.basename(folder) + loader = DirectoryLoader( + folder, glob=f"**/*{file_ext}", loader_cls=TextLoader, loader_kwargs=text_loader_kwargs + ) + folder_docs = loader.load() + documents.extend([add_metadata(doc, doc_type) for doc in folder_docs]) + + text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200) + chunks = text_splitter.split_documents(documents) + + return chunks + + +def create_vector_db(db_name: str, chunks: List[Document], embeddings: Any) -> Any: + """ + Create a vector database from document chunks. + + :param db_name: Name of the database to create. + :type db_name: str + :param chunks: List of document chunks. + :type chunks: List[Document] + :param embeddings: Embedding function to use. + :type embeddings: Any + :return: Created vector store. + :rtype: Any + """ + # Delete if already exists + if os.path.exists(db_name): + Chroma(persist_directory=db_name, embedding_function=embeddings).delete_collection() + + # Create vectorstore + vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, persist_directory=db_name) + + return vectorstore + + +def get_local_vector_db(path: str) -> Any: + """ + Get a local vector database. + + :param path: Path to the local vector database. + :type path: str + :return: Persistent client for the vector database. + :rtype: Any + """ + return PersistentClient(path=path) + + +def get_vector_db_info(vector_store: Any) -> None: + """ + Print information about the vector database. + + :param vector_store: Vector store to get information from. + :type vector_store: Any + """ + collection = vector_store._collection + count = collection.count() + + sample_embedding = collection.get(limit=1, include=["embeddings"])["embeddings"][0] + dimensions = len(sample_embedding) + + print(f"There are {count:,} vectors with {dimensions:,} dimensions in the vector store") + + +def get_plot_data(collection: Any) -> Tuple[np.ndarray, List[str], List[str], List[str]]: + """ + Get plot data from a collection. + + :param collection: Collection to get data from. + :type collection: Any + :return: Tuple containing vectors, colors, document types, and documents. + :rtype: Tuple[np.ndarray, List[str], List[str], List[str]] + """ + result = collection.get(include=['embeddings', 'documents', 'metadatas']) + vectors = np.array(result['embeddings']) + documents = result['documents'] + metadatas = result['metadatas'] + doc_types = [metadata['doc_type'] for metadata in metadatas] + colors = [['blue', 'green', 'red', 'orange'][['products', 'employees', 'contracts', 'company'].index(t)] for t in + doc_types] + + return vectors, colors, doc_types, documents + + +def get_2d_plot(collection: Any) -> go.Figure: + """ + Generate a 2D plot of the vector store. + + :param collection: Collection to generate plot from. + :type collection: Any + :return: 2D scatter plot figure. + :rtype: go.Figure + """ + vectors, colors, doc_types, documents = get_plot_data(collection) + tsne = TSNE(n_components=2, random_state=42) + reduced_vectors = tsne.fit_transform(vectors) + + fig = go.Figure(data=[go.Scatter( + x=reduced_vectors[:, 0], + y=reduced_vectors[:, 1], + mode='markers', + marker=dict(size=5, color=colors, opacity=0.8), + text=[f"Type: {t}
Text: {d[:100]}..." for t, d in zip(doc_types, documents)], + hoverinfo='text' + )]) + + fig.update_layout( + title='2D Chroma Vector Store Visualization', + scene=dict(xaxis_title='x', yaxis_title='y'), + width=800, + height=600, + margin=dict(r=20, b=10, l=10, t=40) + ) + + return fig + + +def get_3d_plot(collection: Any) -> go.Figure: + """ + Generate a 3D plot of the vector store. + + :param collection: Collection to generate plot from. + :type collection: Any + :return: 3D scatter plot figure. + :rtype: go.Figure + """ + vectors, colors, doc_types, documents = get_plot_data(collection) + tsne = TSNE(n_components=3, random_state=42) + reduced_vectors = tsne.fit_transform(vectors) + + fig = go.Figure(data=[go.Scatter3d( + x=reduced_vectors[:, 0], + y=reduced_vectors[:, 1], + z=reduced_vectors[:, 2], + mode='markers', + marker=dict(size=5, color=colors, opacity=0.8), + text=[f"Type: {t}
Text: {d[:100]}..." for t, d in zip(doc_types, documents)], + hoverinfo='text' + )]) + + fig.update_layout( + title='3D Chroma Vector Store Visualization', + scene=dict(xaxis_title='x', yaxis_title='y', zaxis_title='z'), + width=900, + height=700, + margin=dict(r=20, b=10, l=10, t=40) + ) + + return fig + + +def get_conversation_chain(vectorstore: Any) -> ConversationalRetrievalChain: + """ + Create a conversation chain using the vector store. + + :param vectorstore: Vector store to use in the conversation chain. + :type vectorstore: Any + :return: Conversational retrieval chain. + :rtype: ConversationalRetrievalChain + """ + llm = ChatOpenAI(temperature=0.7, model_name=Rag.GPT_MODEL.value) + + memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True, output_key='answer') + + retriever = vectorstore.as_retriever(search_kwargs={"k": 25}) + + conversation_chain = ConversationalRetrievalChain.from_llm( + llm=llm, + retriever=retriever, + memory=memory, + return_source_documents=True, + ) + + return conversation_chain + + +def get_lang_doc(document_text, doc_id, metadata=None, encoding='utf-8'): + + """ + Build a langchain Document that can be used to create a chroma database + + :type document_text: str + :param document_text: + The text to add to a document object + :type doc_id: str + :param doc_id: + The document id to include. + :type metadata: dict + :param metadata: + A dictionary of metadata to associate to the document object. This will help filter an item from a + vector database. + :type encoding: string + :param encoding: + The type of encoding to use for loading the text. + + """ + return Document( + page_content=document_text, + id=doc_id, + metadata=metadata, + encoding=encoding, + ) + + From 83d54044f7a431fce73dc47f86dac116add25537 Mon Sep 17 00:00:00 2001 From: samt07 Date: Fri, 31 Jan 2025 23:23:18 -0500 Subject: [PATCH 05/10] Added test case automation solution --- week1/community-contributions/reqdoc.docx | Bin 0 -> 37730 bytes .../testcase_automation.ipynb | 308 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 week1/community-contributions/reqdoc.docx create mode 100644 week1/community-contributions/testcase_automation.ipynb diff --git a/week1/community-contributions/reqdoc.docx b/week1/community-contributions/reqdoc.docx new file mode 100644 index 0000000000000000000000000000000000000000..0a5a76a458214be1d15275908b90c1feebf1160e GIT binary patch literal 37730 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 p%3Tpiyuds-9^~^2@vF9&V036}B?|1I10:\n", + " print(\"API key looks good!\")\n", + "else:\n", + " print(\"There might be a problem with your API key. Please check!\")\n", + " \n", + "MODEL = 'gpt-4o-mini'\n", + "openai = OpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6110ff3-74bc-430a-8051-7d86a216f0fb", + "metadata": { + "id": "b6110ff3-74bc-430a-8051-7d86a216f0fb" + }, + "outputs": [], + "source": [ + "#Set up system prompt for extracting just the requirements from the document\n", + "\n", + "req_doc_system_prompt = \"You are provided with a complete requirements specifications document. \\\n", + "You are able to decide which content from that document are related to actual requirements, identify each requirement as \\\n", + "functional or non-functional and list them all.\\n\"\n", + "req_doc_system_prompt += \"If the document is empty or do not contain requirements or if you cannot extract them, please respond as such.\\\n", + "Do not make up your own requirements. \\n\"\n", + "req_doc_system_prompt += \"You should respond in JSON as in this example:\"\n", + "req_doc_system_prompt += \"\"\"\n", + "{\n", + " \"requirements\": [\n", + " {\"RequirementNo\": \"FR-01\", \"Requirement Description\": \"description of this functional requirement goes here\"},\n", + " {\"RequirementNo\": \"FR-02\": \"Requirement Description\": \"description of this functional requirement goes here\"},\n", + " {\"RequirementNo\": \"NFR-01\": \"Requirement Description\": \"description of this non-functional requirement goes here\"},\n", + " {\"RequirementNo\": \"NFR-02\": \"Requirement Description\": \"description of this non-functional requirement goes here\"}\n", + " ]\n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20460e45-c1b7-4dc4-ab07-932235c19895", + "metadata": { + "id": "20460e45-c1b7-4dc4-ab07-932235c19895" + }, + "outputs": [], + "source": [ + "#Set up user prompt, sending in the requirements doc as input and calling the ReqDoc.extract function. Key to note here is the explicit instructions to\n", + "#respond in JSON format.\n", + "\n", + "def req_doc_user_prompt(doc):\n", + " user_prompt = \"Here is the contents from a requirement document.\\n\"\n", + " user_prompt += f\"{doc.extract()} \\n\"\n", + " user_prompt += \"Please scan through the document and extract only the actual requirements. For example, ignore sections or \\\n", + "paragraphs such as Approvers, table of contents and similar sections which are not really requirements.\\\n", + "You must respond in a JSON format\"\n", + " user_prompt += \"If the content is empty, respond that there are no valid requirements you could extract and ask for a proper document.\\n\"\n", + " user_prompt = user_prompt[:25_000] # Truncate if more than 25,000 characters\n", + " return user_prompt\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a9f0f84-69a0-4971-a545-5bb40c2f9891", + "metadata": { + "id": "3a9f0f84-69a0-4971-a545-5bb40c2f9891" + }, + "outputs": [], + "source": [ + "#Function to call chatgpt-4o-mini model with the user and system prompts set above and returning the json formatted result obtained from chatgpt\n", + "\n", + "def get_requirements(doc):\n", + " reqdoc = ReqDoc(doc)\n", + " response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": req_doc_system_prompt},\n", + " {\"role\": \"user\", \"content\": req_doc_user_prompt(reqdoc)}\n", + " ],\n", + " response_format={\"type\": \"json_object\"}\n", + " )\n", + " result = response.choices[0].message.content\n", + " return json.loads(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9bb04ef-78d3-4e0f-9ed1-59a961a0663e", + "metadata": { + "id": "f9bb04ef-78d3-4e0f-9ed1-59a961a0663e" + }, + "outputs": [], + "source": [ + "#Uncomment and run this if you want to see the extracted requriements in json format.\n", + "#get_requirements(\"reqdoc.docx\")" + ] + }, + { + "cell_type": "markdown", + "id": "1fe8618c-1dfe-4030-bad8-405731294c93", + "metadata": { + "id": "1fe8618c-1dfe-4030-bad8-405731294c93" + }, + "source": [ + "### Next, we will make another call to gpt-4o-mini" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db2c1eb3-7740-43a4-9c0b-37b7e70c739b", + "metadata": { + "id": "db2c1eb3-7740-43a4-9c0b-37b7e70c739b" + }, + "outputs": [], + "source": [ + "#Set up system prompt to ask for test cases in table format\n", + "\n", + "system_prompt = \"You are an assitant that receives a list of functional and non functional requirements in JSON format. You are the expert in generating unit test cases for each requirement. \\\n", + "You will create as many different test cases as needed for each requirement and produce a result in a table. Order the table by requirement No. Provide clear details on test case pass criteria. \\\n", + "The table will contain the following columns. \\\n", + "1.S No\\\n", + "2.Requirement No\\\n", + "3.Requirement Description\\\n", + "4.Test Case ID\\\n", + "5.Test case summary\\\n", + "6.Test case description\\\n", + "7.Success criteria \\n\"\n", + "system_prompt += \"If you are provided with an empty list, ask for a proper requirement doc\\n\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4cd2bdf-e1bd-43ff-85fa-760ba39ed8c5", + "metadata": { + "id": "c4cd2bdf-e1bd-43ff-85fa-760ba39ed8c5" + }, + "outputs": [], + "source": [ + "# Set up user prompt passing in the req doc file. This in turn will call the get_requirements function, which will make a call to chatgpt.\n", + "\n", + "def get_testcase_user_prompt(reqdoc):\n", + " user_prompt = \"You are looking at the following list of requirements. \\n\"\n", + " user_prompt += f\"{get_requirements(reqdoc)}\\n\"\n", + " user_prompt += \"Prepare unit test cases for each of these requirements in a table and send that table as response. \\n\"\n", + " user_prompt += user_prompt[:25000]\n", + " return user_prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59d859e2-e5bb-4bd6-ab59-5ad967d5d2e0", + "metadata": { + "id": "59d859e2-e5bb-4bd6-ab59-5ad967d5d2e0" + }, + "outputs": [], + "source": [ + "#This is the 2nd call to chatgpt to get test cases. display(Markdown) will take care of producing a neatly formatted table output.\n", + "def create_testcase_doc(reqdoc):\n", + " stream = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": get_testcase_user_prompt(reqdoc)}\n", + " ],\n", + " stream=True\n", + " )\n", + " response = \"\"\n", + " display_handle = display(Markdown(\"\"), display_id=True)\n", + " for chunk in stream:\n", + " response += chunk.choices[0].delta.content or ''\n", + " response = response.replace(\"```\",\"\").replace(\"markdown\", \"\")\n", + " update_display(Markdown(response), display_id=display_handle.display_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0612d662-7047-4620-aa1c-2eb1c3d715cb", + "metadata": { + "id": "0612d662-7047-4620-aa1c-2eb1c3d715cb" + }, + "outputs": [], + "source": [ + "#The final piece of code. Provide the uploaded requirements filename below.\n", + "file_path = r\"reqdoc.docx\"\n", + "#print(file_path)\n", + "create_testcase_doc(file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82ae4371-22dd-4f2a-97c9-a70e0232a0aa", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From ee2e39c2b6c03e1902493b1af3e384cdcf25a493 Mon Sep 17 00:00:00 2001 From: Daniel Emakporuena <97764732+Daniel15568@users.noreply.github.com> Date: Sat, 1 Feb 2025 15:04:47 +0100 Subject: [PATCH 06/10] Create week1-coderesearcher.py --- .../week1-coderesearcher.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 week1/community-contributions/week1-coderesearcher.py diff --git a/week1/community-contributions/week1-coderesearcher.py b/week1/community-contributions/week1-coderesearcher.py new file mode 100644 index 0000000..23c664b --- /dev/null +++ b/week1/community-contributions/week1-coderesearcher.py @@ -0,0 +1,45 @@ +import ollama, os +from openai import OpenAI +from dotenv import load_dotenv +from IPython.display import Markdown, display + +load_dotenv() + +open_key = os.getenv("OPENAI_API_KEY") + +OPEN_MODEL = "gpt-4-turbo" +ollama_model = "llama3.2" +openai = OpenAI() + +system_prompt = "You are an assistant that focuses on the reason for each code, analysing and interpreting what the code does and how it could be improved, \ + Give your answer in markdown down with two different topics namely: Explanation and Code Improvement. However if you think there is no possible improvement \ + to said code, simply state 'no possible improvement '" + +def user_prompt(): + custom_message = input("Write your prompt message: ") + return custom_message + +def explain(): + response = openai.chat.completions.create(model=OPEN_MODEL, + messages = [ + {"role":"system", "content":system_prompt}, + {"role": "user", "content":user_prompt()} + ]) + result = response.choices[0].message.content + display(Markdown(result)) + +# explain() run this to get the openai output with peronalized input + +#With ollama + +ollama_api = "https://localhost:11434/api/chat" + +def explainer_with_ollama(): + response = ollama.chat(model=ollama_model, messages=[ + {"role":"system", "content":system_prompt}, + {"role":"user", "content":user_prompt()} + ]) + result = response["message"]["content"] + display(Markdown(result)) + +#explainer_with_ollama() run for ollama output with same personalized input From 0ab22e11d8ab80cbe8e7e1ebf91f471adf7b9380 Mon Sep 17 00:00:00 2001 From: dsadrianzadeh Date: Sat, 1 Feb 2025 17:24:07 -0500 Subject: [PATCH 07/10] Added my contribution to community-contributions --- .../day01_email_subject_line_en-fr.ipynb | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 week1/community-contributions/day01_email_subject_line_en-fr.ipynb diff --git a/week1/community-contributions/day01_email_subject_line_en-fr.ipynb b/week1/community-contributions/day01_email_subject_line_en-fr.ipynb new file mode 100644 index 0000000..9b272d2 --- /dev/null +++ b/week1/community-contributions/day01_email_subject_line_en-fr.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d25b0aef-3e5e-4026-90ee-2b373bf262b7", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 0: Import libraries and load environment variables\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from IPython.display import Markdown, display\n", + "from openai import OpenAI\n", + "\n", + "load_dotenv(override=True)\n", + "api_key = os.getenv(\"OPENAI_API_KEY\")\n", + "\n", + "if not api_key:\n", + " print(\"No API key was found!\")\n", + "elif not api_key.startswith(\"sk-proj-\"):\n", + " print(\"An API key was found, but it does not start with 'sk-proj-'! Please ensure you are using the right key.\")\n", + "elif api_key.strip() != api_key:\n", + " print(\"An API key was found, but it looks like it might have space or tab characters at the start or end! Please remove them.\")\n", + "else:\n", + " print(\"API key found and looks good so far!\")\n", + "\n", + "# Step 1: Create prompts\n", + "print(\"[INFO] Creating system prompt ...\")\n", + "system_prompt = \"You are an assistant that analyzes the contents of \\\n", + " email texts and suggests short subject lines for the email based \\\n", + " on the requested tone and language. Respond in markdown.\"\n", + "\n", + "print(\"[INFO] Creating user prompt ...\")\n", + "user_prompt = \"\"\"\n", + " The text below is an e-mail text for which you are required to \\\n", + " provide subject lines. Please provide two snarky, two funny, and \\\n", + " two formal short subject lines for the email text. Each of the six \\\n", + " subject lines should be presented in both English and French \\\n", + " languages, making a total of 12 subject lines. Please provide your \\\n", + " answer in markdown.\\\n", + " \n", + " \\n\\n\n", + " \n", + " Welcome to arXiv!\n", + "\n", + " Thank you for creating an account and joining the arXiv community. We look\n", + " forward to receiving your contribution.\n", + "\n", + " Help Pages\n", + " An overview on how to navigate and use arXiv can be found here:\n", + " https://arxiv.org/help\n", + " https://arxiv.org/about\n", + "\n", + " If you would like to know more about the submission process, please go here:\n", + " https://arxiv.org/help/submit\n", + "\n", + " Before Submitting to arXiv\n", + " The arXiv.org e-print archive is fully automated and processes nearly\n", + " 1,000 new submissions per day. To help us keep the process running smoothly\n", + " and efficiently please check your submission carefully for mistakes, typos\n", + " and layout issues. Once you have submitted your work please check your account\n", + " frequently for verification messages and other communication from arXiv.\n", + "\n", + " Contacting arXiv\n", + " We have provided extensive help pages to guide you through the process and\n", + " to answer the most common questions. If you have problems with the submission\n", + " process please contact us here:\n", + " https://arxiv.org/help/contact\n", + " We aim to assist submitters within one business day, but during times of high\n", + " volume or maintenance work we may be slightly delayed in our response.\n", + "\n", + " Thank you for your cooperation.\n", + "\"\"\"\n", + "\n", + "# Step 2: Make messages list\n", + "print(\"[INFO] Making messages list ...\")\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + "]\n", + "\n", + "# Step 3: Call OpenAI\n", + "print(\"[INFO] Calling OpenAI ...\")\n", + "openai = OpenAI()\n", + "response = openai.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=messages\n", + " )\n", + "\n", + "# Step 4: Print result\n", + "print(\"[INFO] Print result ...\")\n", + "display(Markdown(response.choices[0].message.content))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0a6676e-fb43-4725-9389-2acd74c13c4e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 5061c85e3dda5717246364588ffa32207123bfa2 Mon Sep 17 00:00:00 2001 From: Edward Donner Date: Sat, 1 Feb 2025 22:27:15 -0500 Subject: [PATCH 08/10] Fixed typo in troubleshooting --- week1/troubleshooting.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/week1/troubleshooting.ipynb b/week1/troubleshooting.ipynb index 8cebb8c..03032fc 100644 --- a/week1/troubleshooting.ipynb +++ b/week1/troubleshooting.ipynb @@ -107,7 +107,7 @@ " venv_name = os.path.basename(virtual_env)\n", " print(f\"Environment Name: {venv_name}\")\n", "\n", - "if conda_name != \"llms\" and virtual_env != \"llms\":\n", + "if conda_name != \"llms\" and venv_name != \"llms\" and venv_name != \"venv\":\n", " print(\"Neither Anaconda nor Virtualenv seem to be activated with the expected name 'llms'\")\n", " print(\"Did you run 'jupyter lab' from an activated environment with (llms) showing on the command line?\")\n", " print(\"If in doubt, close down all jupyter lab, and follow Part 5 in the SETUP-PC or SETUP-mac guide.\")" From 2b1ed1c9956e0e6430b16f28d1735e36deba17c0 Mon Sep 17 00:00:00 2001 From: Ernest Gaise Date: Sat, 1 Feb 2025 22:57:34 -0500 Subject: [PATCH 09/10] QuizGenerator - CommunityContribution --- .../day1_quiz_generator.ipynb | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 week1/community-contributions/day1_quiz_generator.ipynb diff --git a/week1/community-contributions/day1_quiz_generator.ipynb b/week1/community-contributions/day1_quiz_generator.ipynb new file mode 100644 index 0000000..014674a --- /dev/null +++ b/week1/community-contributions/day1_quiz_generator.ipynb @@ -0,0 +1,170 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 8, + "id": "6ba7c60a-c338-49a1-b1ba-46b7c20e33cb", + "metadata": {}, + "outputs": [], + "source": [ + "import openai\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "from IPython.display import Markdown, display" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4acb4062-17b2-43b1-8b74-aefaa9599463", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "API key found and looks good so far!\n" + ] + } + ], + "source": [ + "load_dotenv(override=True)\n", + "api_key = os.getenv('OPENAI_API_KEY')\n", + "\n", + "# Check the key\n", + "\n", + "if not api_key:\n", + " print(\"No API key was found - please head over to the troubleshooting notebook in this folder to identify & fix!\")\n", + "elif not api_key.startswith(\"sk-proj-\"):\n", + " print(\"An API key was found, but it doesn't start sk-proj-; please check you're using the right key - see troubleshooting notebook\")\n", + "elif api_key.strip() != api_key:\n", + " print(\"An API key was found, but it looks like it might have space or tab characters at the start or end - please remove them - see troubleshooting notebook\")\n", + "else:\n", + " print(\"API key found and looks good so far!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "56f011b2-b759-4ad6-9d01-870fbcb8ade1", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_quiz(topic):\n", + " prompt = f\"Generate a multiple-choice quiz with 5 questions on the topic: {topic}. Include the correct answer for each question.\"\n", + " \n", + " messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a quiz generator. Create a multiple-choice quiz with 5 questions and provide the correct answers.Respond in markdown.\"},\n", + " {\"role\": \"user\", \"content\": prompt}\n", + " ]\n", + " \n", + " response = openai.chat.completions.create(\n", + " model=\"gpt-4\",\n", + " messages=messages,\n", + " max_tokens=300\n", + " )\n", + " \n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1cf977e7-b04b-49e7-8b0a-d0ab2800c234", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Question 1:** What is Python?\n", + "\n", + "**Choice A:** A type of snake\n", + "**Choice B:** A medical term\n", + "**Choice C:** A drilling tool\n", + "**Choice D:** A high-level programming language\n", + "\n", + "Correct Answer: **Choice D:** A high-level programming language\n", + "\n", + "**Question 2:** In Python, what keyword is used to create a function?\n", + "\n", + "**Choice A:** func\n", + "**Choice B:** def\n", + "**Choice C:** function\n", + "**Choice D:** create\n", + "\n", + "Correct Answer: **Choice B:** def\n", + "\n", + "**Question 3:** What is the correct syntax to output \"Hello World\" in Python?\n", + "\n", + "**Choice A:** printf(\"Hello World\")\n", + "**Choice B:** println(\"Hello World\")\n", + "**Choice C:** echo(\"Hello World\")\n", + "**Choice D:** print(\"Hello World\")\n", + "\n", + "Correct Answer: **Choice D:** print(\"Hello World\")\n", + "\n", + "**Question 4:** How would you create a variable \"x\" that equals 5 in Python?\n", + "\n", + "**Choice A:** var x = 5\n", + "**Choice B:** x := 5\n", + "**Choice C:** x = 5\n", + "**Choice D:** x : 5\n", + "\n", + "Correct Answer: **Choice C:** x = 5\n", + "\n", + "**Question 5:** How do you create a comment in Python?\n", + "\n", + "**Choice A:** // This is a comment\n", + "**Choice B:** # This is a comment\n", + "**Choice C:** \n", + "**Choice D:** /* This is a comment */\n", + "\n", + "Correct Answer" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Example usage\n", + "topic = \"Python programming\"\n", + "quiz = generate_quiz(topic)\n", + "display(Markdown(quiz))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70990d7c-6061-43c6-b3c9-9146a3c51c3e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 009a90b7ae7a9a7f7a7eb6824b2b3019f4139e33 Mon Sep 17 00:00:00 2001 From: Emads Date: Sun, 2 Feb 2025 15:18:14 +0200 Subject: [PATCH 10/10] Add contributions to week 4 community-contributions --- .../ems_week4_docupy.ipynb | 869 ++++++++++++++++++ .../ems_week4_trading.ipynb | 528 +++++++++++ 2 files changed, 1397 insertions(+) create mode 100644 week4/community-contributions/ems_week4_docupy.ipynb create mode 100644 week4/community-contributions/ems_week4_trading.ipynb diff --git a/week4/community-contributions/ems_week4_docupy.ipynb b/week4/community-contributions/ems_week4_docupy.ipynb new file mode 100644 index 0000000..88ea725 --- /dev/null +++ b/week4/community-contributions/ems_week4_docupy.ipynb @@ -0,0 +1,869 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "ykDDGx1cjYlh" + }, + "source": [ + "# **DocuPy** \n", + "### _\"Automate Documentation, Comments, and Unit Tests for Python Code\"_ \n", + "\n", + "## Overview \n", + "DocuPy is a Gradio-powered tool designed to automate essential but time-consuming Python development tasks. It streamlines documentation, unit testing, and Python-to-C++ code conversion with AI-driven assistance. \n", + "\n", + "### Key Features \n", + "✅ **Auto-Generate Docstrings & Comments** – Instantly improve code clarity and maintainability. \n", + "✅ **Unit Test Generation** – Ensure reliability with AI-generated test cases. \n", + "✅ **Python to C++ Conversion** – Seamlessly translate Python code to C++ with execution support. \n", + "\n", + "With an intuitive tab-based UI, DocuPy enhances productivity for developers of all levels. Whether you're documenting functions, validating code with tests, or exploring C++ conversions, this tool lets you focus on coding while it handles the rest. \n", + "\n", + "🔗 **Check out the repo**: [GitHub Repo](https://github.com/emads22/DocuPy) \n", + "\n", + "💡 **Have insights, feedback, or ideas?** Feel free to reach out. \n", + "\n", + "[](https://github.com/emads22)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you're running this notebook on **`Google Colab`**, ensure you install the required libraries by running the following command:\n", + "\n", + "```bash\n", + "!pip install -q openai anthropic python-dotenv gradio huggingface_hub transformers\n", + "```\n", + "Otherwise, make sure to activate the Conda environment `docupy` that already includes these modules:\n", + "\n", + "```bash\n", + "conda activate docupy\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6wIpBtNPjXc8" + }, + "outputs": [], + "source": [ + "# Uncomment the following command when running on Google Colab\n", + "# !pip install -q openai anthropic python-dotenv gradio huggingface_hub transformers " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T-cTBf9amBxf" + }, + "source": [ + "## Setup and Install Dependencies\n", + "\n", + "- Start by installing all necessary libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aIHWC7xpk87X" + }, + "outputs": [], + "source": [ + "# imports\n", + "import os\n", + "import io\n", + "import sys\n", + "import subprocess\n", + "import openai\n", + "import anthropic\n", + "import google.generativeai as google_genai\n", + "import gradio as gr\n", + "from openai import OpenAI\n", + "# from google.colab import userdata\n", + "from dotenv import load_dotenv\n", + "from pathlib import Path\n", + "from huggingface_hub import login, InferenceClient\n", + "from transformers import AutoTokenizer" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LZQbXR3dmZy4" + }, + "source": [ + "## Add Secrets to the Colab Notebook\n", + "\n", + "- Add the API keys for OpenAI, Claude, and Gemini to authenticate and access their respective models and services.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AadABekBm4fV" + }, + "outputs": [], + "source": [ + "# # Log in to Hugging Face using the token and add it to git credentials\n", + "# hf_token = userdata.get('HF_TOKEN')\n", + "# login(token=hf_token, add_to_git_credential=True)\n", + "\n", + "# # Endpoint URL for accessing the Code Qwen model through Hugging Face\n", + "# CODE_QWEN_URL = userdata.get('CODE_QWEN_URL')\n", + "\n", + "# # Initialize inference clients with every model using API keys\n", + "# gpt = openai.OpenAI(api_key=userdata.get('OPENAI_API_KEY'))\n", + "# claude = anthropic.Anthropic(api_key=userdata.get('ANTHROPIC_API_KEY'))\n", + "# google_genai.configure(api_key=userdata.get('GOOGLE_API_KEY'))\n", + "# code_qwen = InferenceClient(CODE_QWEN_URL, token=hf_token)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ej3JNfh_wc0m" + }, + "source": [ + "## Alternatively, if not running on Google Colab, Load Environment Variables for API Keys\n", + "\n", + "- Use the `load_dotenv()` function to securely load API keys from a `.env` file.\n", + "- Ensure that the `.env` file is located in the same directory as your script or Jupyter Notebook.\n", + "- The `.env` file should include the required API keys for OpenAI, Claude, and Gemini." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "av9X9XpQw0Vd" + }, + "outputs": [], + "source": [ + "load_dotenv()\n", + "\n", + "# Log in to Hugging Face using the token and add it to git credentials\n", + "hf_token = os.getenv('HF_TOKEN')\n", + "login(token=hf_token, add_to_git_credential=True)\n", + "\n", + "# Endpoint URL for accessing the Code Qwen model through Hugging Face\n", + "CODE_QWEN_URL = os.getenv('CODE_QWEN_URL')\n", + "\n", + "# Initialize inference clients with every model using API keys\n", + "gpt = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))\n", + "claude = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY'))\n", + "google_genai.configure(api_key=os.getenv('GOOGLE_API_KEY'))\n", + "code_qwen = InferenceClient(CODE_QWEN_URL, token=hf_token)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lvEhCuQjrTYu" + }, + "source": [ + "## Define Required Constants\n", + "\n", + "- Initialize the essential constants required for the application's functionality.\n", + "- Configure the system and user prompts specific to each task or feature.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AKEBKKmAowt2" + }, + "outputs": [], + "source": [ + "# Models\n", + "OPENAI_MODEL = \"gpt-4o\"\n", + "CLAUDE_MODEL = \"claude-3-5-sonnet-20240620\"\n", + "GEMINI_MODEL = \"gemini-1.5-pro\"\n", + "CODE_QWEN_MODEL = \"Qwen/CodeQwen1.5-7B-Chat\"\n", + "\n", + "MODELS_IN_USE = [\"GPT\", \"Claude\", \"Gemini\", \"CodeQwen\"]\n", + "\n", + "MAX_TOKENS = 2000\n", + "\n", + "ACTION_A = \"commenting\"\n", + "ACTION_B = \"testing\"\n", + "ACTION_C = \"converting\"\n", + "\n", + "# Define and create the path for the \"temp_files\" directory within the current script's directory\n", + "TEMP_DIR = Path.cwd() / \"temp_files\"\n", + "TEMP_DIR.mkdir(parents=True, exist_ok=True)\n", + "\n", + "PYTHON_SCRIPT_EASY = \"\"\"\n", + "import time\n", + "\n", + "def reverse_string(s):\n", + " return s[::-1]\n", + "\n", + "if __name__ == \"__main__\":\n", + " start_time = time.time()\n", + " text = \"Hello, World!\"\n", + " print(f\"- Original string: {text}\")\n", + " print(\"- Reversed string:\", reverse_string(text))\n", + " execution_time = time.time() - start_time \n", + " print(f\"\\\\n=> Execution Time: {execution_time:.6f} seconds\")\n", + "\"\"\"\n", + "\n", + "PYTHON_SCRIPT_INTERMEDIATE = \"\"\"\n", + "import time\n", + "\n", + "def is_palindrome(s):\n", + " s = s.lower().replace(\" \", \"\") \n", + " return s == s[::-1]\n", + "\n", + "if __name__ == \"__main__\":\n", + " start_time = time.time()\n", + " text = \"Racecar\"\n", + " if is_palindrome(text):\n", + " print(f\"- '{text}' is a palindrome!\")\n", + " else:\n", + " print(f\"- '{text}' is Not a palindrome.\")\n", + " execution_time = time.time() - start_time \n", + " print(f\"\\\\n=> Execution Time: {execution_time:.6f} seconds\")\n", + "\"\"\"\n", + "\n", + "PYTHON_SCRIPT_HARD = \"\"\"\n", + "import time\n", + "\n", + "def generate_primes(limit):\n", + " primes = []\n", + " for num in range(2, limit + 1):\n", + " if all(num % p != 0 for p in primes):\n", + " primes.append(num)\n", + " return primes\n", + "\n", + "if __name__ == \"__main__\":\n", + " start_time = time.time()\n", + " n = 20\n", + " print(f\"- Generating primes up to: {n}\")\n", + " print(\"- Prime numbers:\", generate_primes(n))\n", + " execution_time = time.time() - start_time \n", + " print(f\"\\\\n=> Execution Time: {execution_time:.6f} seconds\")\n", + "\"\"\"\n", + "\n", + "PYTHON_SCRIPTS = {\n", + " \"reverse_string\" : PYTHON_SCRIPT_EASY,\n", + " \"is_palindrome\" : PYTHON_SCRIPT_INTERMEDIATE,\n", + " \"generate_primes\" : PYTHON_SCRIPT_HARD,\n", + " \"custom\" : \"\"\"\n", + "# Write your custom Python script here\n", + "if __name__ == \"__main__\":\n", + " print(\"Hello, World!\")\n", + "\"\"\"\n", + "}\n", + "\n", + "# Relative system prompts\n", + "SYSTEM_PROMPT_COMMENTS = \"\"\"\n", + "You are an AI model specializing in enhancing Python code documentation.\n", + "Generate detailed and precise docstrings and inline comments for the provided Python code.\n", + "Ensure the docstrings clearly describe the purpose, parameters, and return values of each function.\n", + "Inline comments should explain complex or non-obvious code segments.\n", + "Do not include any introductions, explanations, conclusions, or additional context.\n", + "Return only the updated Python code enclosed within ```python ... ``` for proper formatting and syntax highlighting.\n", + "\"\"\"\n", + "\n", + "SYSTEM_PROMPT_TESTS = \"\"\"\n", + "You are an AI model specializing in generating comprehensive unit tests for Python code.\n", + "Create Python unit tests that thoroughly validate the functionality of the given code.\n", + "Use the `unittest` framework and ensure edge cases and error conditions are tested.\n", + "Do not include any comments, introductions, explanations, conclusions, or additional context.\n", + "Return only the unit test code enclosed within ```python ... ``` for proper formatting and syntax highlighting.\n", + "\"\"\"\n", + "\n", + "SYSTEM_PROMPT_CONVERT = \"\"\"\n", + "You are an AI model specializing in high-performance code translation.\n", + "Translate the given Python code into equivalent, optimized C++ code.\n", + "Focus on:\n", + "- Using efficient data structures and algorithms.\n", + "- Avoiding unnecessary memory allocations and computational overhead.\n", + "- Ensuring minimal risk of integer overflow by using appropriate data types.\n", + "- Leveraging the C++ Standard Library (e.g., ``, ``) for performance and readability.\n", + "Produce concise and efficient C++ code that matches the functionality of the original Python code.\n", + "Do not include any comments, introductions, explanations, conclusions, or additional context..\n", + "Return only the C++ code enclosed within ```cpp ... ``` for proper formatting and syntax highlighting.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJ1zttf7ANqD" + }, + "outputs": [], + "source": [ + "# Relative user prompts\n", + "def user_prompt_comments(python_code):\n", + " user_prompt = f\"\"\"\n", + "Add detailed docstrings and inline comments to the following Python code:\n", + "\n", + "```python\n", + "{python_code}\n", + "```\n", + "\"\"\"\n", + " return user_prompt\n", + "\n", + "def user_prompt_tests(python_code):\n", + " user_prompt = f\"\"\"\n", + "Generate unit tests for the following Python code using the `unittest` framework:\n", + "\n", + "```python\n", + "{python_code}\n", + "```\n", + "\"\"\"\n", + " return user_prompt\n", + "\n", + "def user_prompt_convert(python_code):\n", + " user_prompt = f\"\"\"\n", + "Convert the following Python code into C++:\n", + "\n", + "```python\n", + "{python_code}\n", + "``` \n", + "\"\"\"\n", + " return user_prompt" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tqrOO_qsCRkd" + }, + "source": [ + "### Define the Tab Functions\n", + "\n", + "- Develop dedicated functions for each service: documenting Python code, generating unit tests, and converting Python to C++.\n", + "- Structure each function to handle user input, process it using the selected AI model, and display the generated output seamlessly.\n", + "- Ensure the functionality of each tab aligns with its specific purpose, providing an intuitive and efficient user experience.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HBsBrq3G94ul" + }, + "outputs": [], + "source": [ + "def stream_gpt(system_prompt, user_prompt):\n", + " stream = gpt.chat.completions.create(\n", + " model=OPENAI_MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ],\n", + " stream=True)\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " reply += chunk.choices[0].delta.content or \"\"\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```cpp\\n\", \"\").replace(\"```\", \"\")\n", + "\n", + "def stream_claude(system_prompt, user_prompt):\n", + " response = claude.messages.stream(\n", + " model=CLAUDE_MODEL,\n", + " max_tokens=MAX_TOKENS,\n", + " system=system_prompt,\n", + " messages=[{\"role\": \"user\", \"content\": user_prompt}],\n", + " )\n", + " reply = \"\"\n", + " with response as stream:\n", + " for text in stream.text_stream:\n", + " reply += text\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```cpp\\n\", \"\").replace(\"```\", \"\")\n", + "\n", + "def stream_gemini(system_prompt, user_prompt):\n", + " gemini = google_genai.GenerativeModel(\n", + " model_name=GEMINI_MODEL,\n", + " system_instruction=system_prompt\n", + " )\n", + " stream = gemini.generate_content(\n", + " contents=user_prompt,\n", + " stream=True\n", + " )\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " reply += chunk.text or \"\"\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```cpp\\n\", \"\").replace(\"```\", \"\")\n", + "\n", + "def stream_code_qwen(system_prompt, user_prompt):\n", + " tokenizer = AutoTokenizer.from_pretrained(CODE_QWEN_MODEL)\n", + " model_input = tokenizer.apply_chat_template(\n", + " conversation=[\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ],\n", + " tokenize=False,\n", + " add_generation_prompt=True\n", + " )\n", + " stream = code_qwen.text_generation(\n", + " prompt=model_input,\n", + " stream=True,\n", + " details=True,\n", + " max_new_tokens=MAX_TOKENS\n", + " )\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " reply += chunk.token.text or \"\"\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```cpp\\n\", \"\").replace(\"```\", \"\")\n", + "\n", + "def set_prompts(user_input, action):\n", + " action = action.lower()\n", + "\n", + " if action == ACTION_A.lower():\n", + " system_prompt = SYSTEM_PROMPT_COMMENTS\n", + " user_prompt = user_prompt_comments(user_input)\n", + " elif action == ACTION_B.lower():\n", + " system_prompt = SYSTEM_PROMPT_TESTS\n", + " user_prompt = user_prompt_tests(user_input)\n", + " elif action == ACTION_C.lower():\n", + " system_prompt = SYSTEM_PROMPT_CONVERT\n", + " user_prompt = user_prompt_convert(user_input)\n", + " else:\n", + " return None, None\n", + " \n", + " return system_prompt, user_prompt\n", + "\n", + "def stream_response(user_input, model, action):\n", + " system_prompt, user_prompt = set_prompts(user_input, action)\n", + " if not all((system_prompt, user_prompt)):\n", + " raise ValueError(\"Unknown Action\")\n", + "\n", + " match model:\n", + " case \"GPT\":\n", + " yield from stream_gpt(system_prompt, user_prompt)\n", + "\n", + " case \"Claude\":\n", + " yield from stream_claude(system_prompt, user_prompt)\n", + "\n", + " case \"Gemini\":\n", + " yield from stream_gemini(system_prompt, user_prompt)\n", + "\n", + " case \"CodeQwen\":\n", + " yield from stream_code_qwen(system_prompt, user_prompt)\n", + " \n", + "def generate_comments(python_code, selected_model):\n", + " for model in MODELS_IN_USE:\n", + " if model == selected_model:\n", + " yield from stream_response(python_code, model, action=ACTION_A)\n", + " return # Exit the function immediately after exhausting the generator\n", + " raise ValueError(\"Unknown Model\")\n", + "\n", + "def generate_tests(python_code, selected_model):\n", + " for model in MODELS_IN_USE:\n", + " if model == selected_model:\n", + " yield from stream_response(python_code, model, action=ACTION_B)\n", + " return # Exit the function immediately after exhausting the generator\n", + " raise ValueError(\"Unknown Model\")\n", + "\n", + "def convert_code(python_code, selected_model):\n", + " for model in MODELS_IN_USE:\n", + " if model == selected_model:\n", + " yield from stream_response(python_code, model, action=ACTION_C)\n", + " return # Exit the function immediately after exhausting the generator\n", + " raise ValueError(\"Unknown Model\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running Code Functions\n", + "\n", + "- Functions that dynamically execute Python or C++ code provided as a string and captures its output.\n", + "- This is useful for evaluating Python or C++ code snippets and returning their results programmatically.\n", + "\n", + "### IMPORTANT WARNING:\n", + "The functions that dynamically execute Python or C++ code provided as input.\n", + "While powerful, this is extremely dangerous if the input code is not trusted.\n", + "Any malicious code can be executed, including:\n", + " - Deleting files or directories\n", + " - Stealing sensitive data (e.g., accessing environment variables or credentials)\n", + " - Running arbitrary commands that compromise the system\n", + "\n", + "Sharing this notebook with this code snippet can allow attackers to exploit this functionality \n", + "by passing harmful code as input. \n", + "\n", + "If you share this notebook or use this function:\n", + " 1. Only accept input from trusted sources.\n", + " 2. Consider running the code in a sandboxed environment (e.g., virtual machine or container).\n", + " 3. Avoid using this function in publicly accessible applications or notebooks without strict validation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def run_python_exec(code):\n", + " try:\n", + " # Capture stdout using StringIO\n", + " output = io.StringIO()\n", + "\n", + " # Redirect stdout to StringIO\n", + " sys.stdout = output\n", + "\n", + " # Execute the provided Python code\n", + " exec(code)\n", + " finally:\n", + " # Restore original stdout\n", + " sys.stdout = sys.__stdout__\n", + "\n", + " # Return the captured output\n", + " return output.getvalue()\n", + "\n", + "# Improved running python function\n", + "def run_python(code):\n", + " # Save the Python code to a file\n", + " with open(TEMP_DIR / \"python_code.py\", \"w\") as python_file:\n", + " python_file.write(code)\n", + "\n", + " try:\n", + " # Execute the Python code\n", + " result = subprocess.run(\n", + " [\"python\", str(TEMP_DIR / \"python_code.py\")],\n", + " check=True, text=True, capture_output=True\n", + " )\n", + "\n", + " # Return the program's output\n", + " return result.stdout\n", + "\n", + " except subprocess.CalledProcessError as e:\n", + " # Handle compilation or execution errors\n", + " return f\"An error occurred during execution:\\n{e.stderr}\"\n", + "\n", + " finally:\n", + " # Clean up: Delete the Python code file and executable\n", + " file_path = TEMP_DIR / \"python_code.py\"\n", + " if file_path.exists():\n", + " file_path.unlink()\n", + "\n", + "def run_cpp(code):\n", + " # Save the C++ code to a file\n", + " with open(TEMP_DIR / \"cpp_code.cpp\", \"w\") as cpp_file:\n", + " cpp_file.write(code)\n", + "\n", + " try:\n", + " # Compile the C++ code\n", + " subprocess.run(\n", + " [\"g++\", \"-o\", str(TEMP_DIR / \"cpp_code\"), str(TEMP_DIR / \"cpp_code.cpp\")],\n", + " check=True, text=True, capture_output=True\n", + " )\n", + "\n", + " # Execute the compiled program\n", + " result = subprocess.run(\n", + " [str(TEMP_DIR / \"cpp_code\")],\n", + " check=True, text=True, capture_output=True\n", + " )\n", + "\n", + " # Return the program's output\n", + " return result.stdout\n", + "\n", + " except subprocess.CalledProcessError as e:\n", + " # Handle compilation or execution errors\n", + " error_context = \"during compilation\" if \"cpp_code.cpp\" in e.stderr else \"during execution\"\n", + " return f\"An error occurred {error_context}:\\n{e.stderr}\"\n", + "\n", + " finally:\n", + " # Clean up: Delete the C++ source file and executable\n", + " for filename in [\"cpp_code.cpp\", \"cpp_code\", \"cpp_code.exe\"]:\n", + " file_path = TEMP_DIR / filename\n", + " if file_path.exists():\n", + " file_path.unlink()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vude1jzPrgT2" + }, + "source": [ + "## Develop a User-Friendly Interface with Gradio\n", + "\n", + "- Design a clean, intuitive, and user-centric interface using Gradio.\n", + "- Ensure responsiveness and accessibility to provide a seamless and efficient user experience.\n", + "- Focus on simplicity while maintaining functionality to cater to diverse user needs.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Eh-sWFZVBb_y" + }, + "outputs": [], + "source": [ + "# CSS styles for customizing the appearance of the Gradio UI elements.\n", + "css = \"\"\"\n", + ".python { \n", + " background-color: #377ef0; \n", + " color: #ffffff; \n", + " padding: 0.5em; \n", + " border-radius: 5px; /* Slightly rounded corners */\n", + "}\n", + ".cpp { \n", + " background-color: #00549e; \n", + " color: #ffffff; \n", + " padding: 0.5em; \n", + " border-radius: 5px; \n", + "}\n", + ".model { \n", + " background-color: #17a2b8; /* Vibrant cyan color */\n", + " color: white; \n", + " font-size: 1.2em; \n", + " padding: 0.5em; \n", + " border: none; \n", + " border-radius: 5px; \n", + " cursor: pointer; \n", + "}\n", + ".button { \n", + " height: 4em; \n", + " font-size: 1.5em; \n", + " padding: 0.5em 1em; \n", + " background-color: #e67e22; /* Vibrant orange */\n", + " color: white; \n", + " border: none; \n", + " border-radius: 5px; \n", + " cursor: pointer; \n", + "}\n", + ".run-button { \n", + " height: 3em; \n", + " font-size: 1.5em; \n", + " padding: 0.5em 1em; \n", + " background-color: #16a085; /* Rich teal color */\n", + " color: white; \n", + " border: none; \n", + " border-radius: 5px; \n", + " cursor: pointer; \n", + "}\n", + ".button:hover, .run-button:hover {\n", + " background-color: #2c3e50; /* Dark navy for hover effect */\n", + " color: #fff; \n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "M_v-j-B_sQHe" + }, + "outputs": [], + "source": [ + "# Tab to Document Code with Docstrings and Comments\n", + "def docs_comments_ui():\n", + " with gr.Tab(\"Docstrings & Comments\"):\n", + " gr.Markdown(\"\"\"\n", + " ## Document Code with Docstrings and Comments\n", + " This tab allows you to automatically generate docstrings and inline comments for your Python code.\n", + " - Paste your Python code into the **`Python Code`** textbox.\n", + " - Select your preferred model (GPT, Claude, Gemini, or CodeQwen) to process the code.\n", + " - Click the **`Add Docstrings & Comments`** button to generate well-documented Python code.\n", + " The generated code will appear in the **`Python Code with Docstrings and Comments`** textarea.\n", + " \"\"\")\n", + " with gr.Row():\n", + " python = gr.Textbox(label=\"Python Code:\", lines=20, value=PYTHON_SCRIPTS[\"custom\"], elem_classes=[\"python\"])\n", + " python_with_comments = gr.TextArea(label=\"Python Code with Docstrings and Comments:\", interactive=True, lines=20, elem_classes=[\"python\"])\n", + " with gr.Row():\n", + " python_script = gr.Dropdown(choices=list(PYTHON_SCRIPTS.keys()), label=\"Select a Python script\", value=\"custom\", elem_classes=[\"model\"])\n", + " comments_btn = gr.Button(\"Add Docstrings & Comments\", elem_classes=[\"button\"])\n", + " model = gr.Dropdown([\"GPT\", \"Claude\", \"Gemini\", \"CodeQwen\"], label=\"Select Model\", value=\"GPT\", elem_classes=[\"model\"])\n", + " \n", + " python_script.change(\n", + " fn=lambda script: PYTHON_SCRIPTS[script],\n", + " inputs=[python_script],\n", + " outputs=[python]\n", + " )\n", + " \n", + " comments_btn.click(\n", + " fn=lambda: \"\",\n", + " inputs=None,\n", + " outputs=[python_with_comments]\n", + " ).then(\n", + " fn=generate_comments,\n", + " inputs=[python, model],\n", + " outputs=[python_with_comments]\n", + " )\n", + "\n", + " return python_with_comments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WDjJp1eXtQzY" + }, + "outputs": [], + "source": [ + "# Tab to Generate Comprehensive Unit Tests\n", + "def unit_tests_ui():\n", + " with gr.Tab(\"Unit Tests\"):\n", + " gr.Markdown(\"\"\"\n", + " ## Generate Comprehensive Unit Tests\n", + " This tab helps you create unit tests for your Python code automatically.\n", + " - Paste your Python code into the **`Python Code`** textbox.\n", + " - Choose a model (GPT, Claude, Gemini, or CodeQwen) to generate the unit tests.\n", + " - Click the **`Generate Unit Tests`** button, and the generated unit tests will appear in the **`Python Code with Unit Tests`** textarea.\n", + " Use these unit tests to ensure your code behaves as expected.\n", + " \"\"\")\n", + " with gr.Row():\n", + " python = gr.Textbox(label=\"Python Code:\", lines=20, value=PYTHON_SCRIPTS[\"custom\"], elem_classes=[\"python\"])\n", + " python_unit_tests = gr.TextArea(label=\"Python Code with Unit Tests:\", interactive=True, lines=20, elem_classes=[\"python\"])\n", + " with gr.Row():\n", + " python_script = gr.Dropdown(choices=list(PYTHON_SCRIPTS.keys()), label=\"Select a Python script\", value=\"custom\", elem_classes=[\"model\"])\n", + " unit_tests_btn = gr.Button(\"Generate Unit Tests\", elem_classes=[\"button\"])\n", + " model = gr.Dropdown([\"GPT\", \"Claude\", \"Gemini\", \"CodeQwen\"], label=\"Select Model\", value=\"GPT\", elem_classes=[\"model\"])\n", + " \n", + " python_script.change(\n", + " fn=lambda script: PYTHON_SCRIPTS[script],\n", + " inputs=[python_script],\n", + " outputs=[python]\n", + " )\n", + " \n", + " unit_tests_btn.click(\n", + " fn=lambda: \"\",\n", + " inputs=None,\n", + " outputs=[python_unit_tests]\n", + " ).then(\n", + " fn=generate_tests,\n", + " inputs=[python, model],\n", + " outputs=[python_unit_tests]\n", + " )\n", + "\n", + " return python_unit_tests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x57SZeLi9NyV" + }, + "outputs": [], + "source": [ + "# Tab to Convert Python Code to C++\n", + "def python_to_cpp_ui():\n", + " with gr.Tab(\"Python to C++\"):\n", + " gr.Markdown(\"\"\"\n", + " ## Convert Python Code to C++\n", + " This tab facilitates the conversion of Python code into C++.\n", + " - Paste your Python code into the **`Python Code`** textbox.\n", + " - Select your preferred model (GPT, Claude, Gemini, or CodeQwen) to perform the conversion.\n", + " - Click **`Convert to C++`** to see the equivalent C++ code in the **`C++ Code`** textbox.\n", + " Additional Features:\n", + " - You can execute the Python or C++ code directly using the respective **`Run Python`** or **`Run C++`** buttons.\n", + " - The output will appear in the respective result text areas below.\n", + " \"\"\")\n", + " with gr.Row():\n", + " python = gr.Textbox(label=\"Python Code:\", lines=20, value=PYTHON_SCRIPTS[\"custom\"], elem_classes=[\"python\"])\n", + " cpp = gr.Textbox(label=\"C++ Code:\", interactive=True, lines=20, elem_classes=[\"cpp\"])\n", + " with gr.Row():\n", + " python_script = gr.Dropdown(choices=list(PYTHON_SCRIPTS.keys()), label=\"Select a Python script\", value=\"custom\", elem_classes=[\"model\"])\n", + " convert_btn = gr.Button(\"Convert to C++\", elem_classes=[\"button\"])\n", + " model = gr.Dropdown([\"GPT\", \"Claude\", \"Gemini\", \"CodeQwen\"], label=\"Select Model\", value=\"GPT\", elem_classes=[\"model\"])\n", + " with gr.Row():\n", + " run_python_btn = gr.Button(\"Run Python\", elem_classes=[\"run-button\"])\n", + " run_cpp_btn = gr.Button(\"Run C++\", elem_classes=[\"run-button\"])\n", + " with gr.Row():\n", + " python_out = gr.TextArea(label=\"Python Result:\", lines=10, elem_classes=[\"python\"])\n", + " cpp_out = gr.TextArea(label=\"C++ Result:\", lines=10, elem_classes=[\"cpp\"])\n", + "\n", + " python_script.change(\n", + " fn=lambda script: PYTHON_SCRIPTS[script],\n", + " inputs=[python_script],\n", + " outputs=[python]\n", + " )\n", + " \n", + " convert_btn.click(\n", + " fn=lambda: \"\",\n", + " inputs=None,\n", + " outputs=[cpp]\n", + " ).then(\n", + " fn=convert_code,\n", + " inputs=[python, model],\n", + " outputs=[cpp]\n", + " )\n", + " run_python_btn.click(run_python, inputs=[python], outputs=[python_out])\n", + " run_cpp_btn.click(run_cpp, inputs=[cpp], outputs=[cpp_out])\n", + "\n", + " return cpp, python_out, cpp_out" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 645 + }, + "id": "n8ZdDrOrrbl-", + "outputId": "08350d69-569e-4947-8da1-d755e9a2678f" + }, + "outputs": [], + "source": [ + "# Combine the tabs into the main UI and handle tab switching\n", + "with gr.Blocks(css=css) as main_ui:\n", + " with gr.Tabs() as tabs:\n", + " comments_output = docs_comments_ui()\n", + " tests_output = unit_tests_ui()\n", + " cpp_output, python_out, cpp_out = python_to_cpp_ui()\n", + "\n", + " # Reset outputs on tab switch\n", + " tabs.select(\n", + " fn=lambda: [\"\", \"\", \"\", \"\", \"\"],\n", + " inputs=None,\n", + " outputs=[comments_output, \n", + " tests_output, \n", + " cpp_output, python_out, cpp_out]\n", + " )\n", + "\n", + "# Launch the app\n", + "main_ui.launch(inbrowser=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/week4/community-contributions/ems_week4_trading.ipynb b/week4/community-contributions/ems_week4_trading.ipynb new file mode 100644 index 0000000..a2460d3 --- /dev/null +++ b/week4/community-contributions/ems_week4_trading.ipynb @@ -0,0 +1,528 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a6ab9a2-28a2-445d-8512-a0dc8d1b54e9", + "metadata": {}, + "source": [ + "# Trading Decision Simulator\n", + "\n", + "## Description\n", + "This document provides Python functions to simulate trading decisions using a predefined API. The API includes stock tickers, historical prices, and a `Trade` class to represent buy or sell actions. Each function demonstrates a unique trading strategy, such as momentum-based trading, mean reversion, portfolio diversification, and more. These examples can serve as a foundation for developing or testing algorithmic trading systems.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e610bf56-a46e-4aff-8de1-ab49d62b1ad3", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "from huggingface_hub import login, InferenceClient\n", + "from transformers import AutoTokenizer\n", + "import google.generativeai as google_genai\n", + "import anthropic\n", + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f672e1c-87e9-4865-b760-370fa605e614", + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up environment\n", + "load_dotenv()\n", + "os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', 'your-key-if-not-using-env')\n", + "os.environ['ANTHROPIC_API_KEY'] = os.getenv('ANTHROPIC_API_KEY', 'your-key-if-not-using-env')\n", + "os.environ['GOOGLE_API_KEY'] = os.getenv('GOOGLE_API_KEY', 'your-key-if-not-using-env')\n", + "os.environ['HF_TOKEN'] = os.getenv('HF_TOKEN', 'your-key-if-not-using-env')\n", + "os.environ['CODE_QWEN_URL'] = os.getenv('CODE_QWEN_URL', 'your-url-if-not-using-env')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8aa149ed-9298-4d69-8fe2-8f5de0f667da", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize\n", + "openai = OpenAI()\n", + "claude = anthropic.Anthropic()\n", + "google_genai.configure()\n", + "code_qwen = InferenceClient(CODE_QWEN_URL)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbb4319c-870f-4c04-99e2-6f54c650537a", + "metadata": {}, + "outputs": [], + "source": [ + "# Constants \n", + "MODELS = {\n", + " \"GPT\": \"gpt-4o\", \n", + " \"Claude\": \"claude-3-5-sonnet-20240620\", \n", + " \"Gemini\": \"gemini-1.5-pro\", \n", + " \"CodeQwen\": \"Qwen/CodeQwen1.5-7B-Chat\"\n", + "}\n", + "\n", + "MAX_TOKENS = 2000\n", + "\n", + "SYSTEM_PROMPT = \"\"\"\n", + "You are an advanced code generation assistant capable of creating high-quality Python code for financial trading systems. \n", + "Your task is to generate Python functions that simulate trading decisions based on the following API:\n", + "\n", + "API DETAILS:\n", + "1. tickers: A list of stock tickers (strings) representing available stocks.\n", + "2. prices: A dictionary where the key is a stock ticker (string) and the value is a list of historical prices (floats). The list is ordered with the most recent price first.\n", + "3. Trade: A class used to represent trading actions.\n", + " - `Trade(ticker, quantity)` creates a trade object:\n", + " - Positive `quantity` (e.g., `100`) represents buying shares.\n", + " - Negative `quantity` (e.g., `-50`) represents selling/shorting shares.\n", + "\n", + "INSTRUCTIONS:\n", + "- You will be provided with an example Python function to demonstrate the API.\n", + "- Your job is to generate 5 additional Python functions, each implementing a unique trading strategy.\n", + "- Ensure the functions are named sequentially (e.g., `trade2()`, `trade3()`, etc.).\n", + "- Include clear comments explaining the logic behind each function.\n", + "- Return a list of `Trade` objects from each function.\n", + "- The output should only include the Python code. Do not include any introductions, conclusions, summaries, or additional context.\n", + "\n", + "CONSIDERATIONS FOR TRADING STRATEGIES:\n", + "- Momentum-based strategies (e.g., trading based on price trends).\n", + "- Mean reversion strategies (e.g., identifying overbought or oversold stocks).\n", + "- Randomized strategies (e.g., simulating stochastic decision-making).\n", + "- Portfolio diversification (e.g., distributing trades across multiple tickers).\n", + "- Risk management strategies (e.g., limiting losses or locking in profits).\n", + "\n", + "EXAMPLE FUNCTION:\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7b3546-57aa-4c29-bc5d-f211970d04eb", + "metadata": {}, + "outputs": [], + "source": [ + "def user_prompt(example_function):\n", + " \"\"\"\n", + " Returns a user prompt for the model by appending the provided example function.\n", + " \"\"\"\n", + " return f\"\"\"\n", + "{example_function}\n", + "\n", + "TASK:\n", + "Based on the provided example function and API, please write 5 additional trading functions named `trade2()`, `trade3()`, and so on. Each function should implement a unique trading strategy as outlined in the system prompt. Make sure each function has clear comments explaining the logic and returns a list of `Trade` objects.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "455728fd-be9b-4d7a-88f2-3afcf026303e", + "metadata": {}, + "source": [ + "# Trade Function Example: `trade1`\n", + "\n", + "This Python script demonstrates a simple trading strategy implemented using a provided API. The `trade1` function identifies the top-performing stock over the last 5 days based on its average price and creates a trade object to buy 100 shares of the selected stock. The function leverages the following components:\n", + "\n", + "- **`tickers`**: A list of available stock tickers.\n", + "- **`prices`**: A dictionary containing historical prices for each stock.\n", + "- **`Trade`**: A class used to represent trading actions (buy or sell).\n", + "- **`numpy`**: Used to calculate average prices efficiently.\n", + "\n", + "The example highlights a momentum-based strategy where the stock with the best recent performance is selected for trading.\n", + "\n", + "example:\n", + "```python\n", + "# Importing the required modules and classes for the trading simulation\n", + "\n", + "# `tickers` is a list of stock tickers (strings), representing available stocks to trade.\n", + "import tickers\n", + "\n", + "# `prices` is a dictionary where:\n", + "# - The key is a stock ticker (string).\n", + "# - The value is a list of historical prices (floats), ordered with the most recent price first.\n", + "import prices\n", + "\n", + "# `Trade` is a class that represents a trading decision. It takes two arguments:\n", + "# - `ticker`: A string representing the stock ticker.\n", + "# - `quantity`: An integer representing the number of shares to buy (positive) or sell/short (negative).\n", + "# Example usage:\n", + "# Trade(\"IBM\", 100) -> Buys 100 shares of IBM stock.\n", + "# Trade(\"IBM\", -50) -> Sells or shorts 50 shares of IBM stock.\n", + "import Trade\n", + "\n", + "# Additional modules for random number generation and numerical operations\n", + "import random\n", + "import numpy as np\n", + "\n", + "def trade1():\n", + " \"\"\"\n", + " Buys the top-performing stock based on its average price over the last 5 days.\n", + "\n", + " Strategy:\n", + " - Calculate the average price of the last 5 days for each stock in `tickers`.\n", + " - Identify the stock with the highest average price.\n", + " - Create a trade object to buy 100 shares of the identified stock.\n", + " \n", + " Returns:\n", + " list[Trade]: A list containing a single trade object for the chosen stock.\n", + " \"\"\"\n", + " # Calculate the 5-day average price for each stock\n", + " avg_prices = {ticker: np.mean(prices[ticker][:5]) for ticker in tickers}\n", + "\n", + " # Find the stock ticker with the highest 5-day average price\n", + " best_ticker = max(avg_prices, key=avg_prices.get)\n", + "\n", + " # Create a trade object to buy 100 shares of the top-performing stock\n", + " trade = Trade(best_ticker, 100)\n", + "\n", + " # Return the trade as a list\n", + " return [trade]\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce0fa282-4e07-4bf8-8b49-7cf7ef4a7572", + "metadata": {}, + "outputs": [], + "source": [ + "# A trading function example\n", + "TRADING_FUNCTION_EXAMPLE = \"\"\"\n", + "# tickers is a list of stock tickers (strings)\n", + "import tickers\n", + "\n", + "# prices is a dict; the key is a ticker and the value is a list of historic prices, today first\n", + "import prices\n", + "\n", + "# Trade represents a decision to buy or sell a quantity of a ticker\n", + "# Trade(\"IBM\", 100) for a trade object representing purchasing 100 shares of IBM stock\n", + "# Trade(\"IBM\", -50) for a trade object representing selling or shorting 50 shares of IBM stock\n", + "import Trade\n", + "\n", + "import random\n", + "import numpy as np\n", + "\n", + "def trade1():\n", + " # Buy top performing stock in the last 5 days\n", + " avg_prices = {ticker: np.mean(prices[ticker][:5]) for ticker in tickers}\n", + " best_ticker = max(avg_prices, key=avg_prices.get)\n", + " trade = Trade(best_ticker, 100)\n", + " return [trade]\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0be9f47d-5213-4700-b0e2-d444c7c738c0", + "metadata": {}, + "outputs": [], + "source": [ + "# UI function to trade using GPT\n", + "def trade_gpt(function_example): \n", + " stream = openai.chat.completions.create(\n", + " model=MODELS[\"GPT\"], \n", + " messages=[\n", + " {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n", + " {\"role\": \"user\", \"content\": user_prompt(function_example)}\n", + " ], \n", + " stream=True\n", + " )\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " reply += chunk.choices[0].delta.content or \"\"\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```\", \"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8669f56b-8314-4582-a167-78842caea131", + "metadata": {}, + "outputs": [], + "source": [ + "# UI function to trade using Claude\n", + "def trade_claude(function_example):\n", + " result = claude.messages.stream(\n", + " model=MODELS[\"Claude\"],\n", + " max_tokens=MAX_TOKENS,\n", + " system=SYSTEM_PROMPT,\n", + " messages=[{\"role\": \"user\", \"content\": user_prompt(function_example)}],\n", + " )\n", + " reply = \"\"\n", + " with result as stream:\n", + " for text in stream.text_stream:\n", + " reply += text\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```\", \"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d27456d0-5cd3-4c2c-a12a-176d53142752", + "metadata": {}, + "outputs": [], + "source": [ + "# UI function to trade using Gemini\n", + "def trade_gemini(function_example):\n", + " gemini = google_genai.GenerativeModel(\n", + " model_name=MODELS[\"Gemini\"],\n", + " system_instruction=SYSTEM_PROMPT\n", + " )\n", + " stream = gemini.generate_content(\n", + " contents=user_prompt(function_example),\n", + " stream=True\n", + " )\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " reply += chunk.text or \"\"\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```\", \"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a9fb676-83c3-452e-abeb-8712ebdee1d1", + "metadata": {}, + "outputs": [], + "source": [ + "# UI function to trade using CodeQwen\n", + "def trade_code_qwen(function_example):\n", + " tokenizer = AutoTokenizer.from_pretrained(MODELS[\"CodeQwen\"])\n", + " model_input = tokenizer.apply_chat_template(\n", + " conversation=[\n", + " {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n", + " {\"role\": \"user\", \"content\": user_prompt(function_example)}\n", + " ],\n", + " tokenize=False,\n", + " add_generation_prompt=True\n", + " )\n", + " stream = code_qwen.text_generation(\n", + " prompt=model_input,\n", + " stream=True,\n", + " details=True,\n", + " max_new_tokens=MAX_TOKENS\n", + " )\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " reply += chunk.token.text or \"\"\n", + " yield reply.replace(\"```python\\n\", \"\").replace(\"```\", \"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f1ae8f5-16c8-40a0-aa18-63b617df078d", + "metadata": {}, + "outputs": [], + "source": [ + "# UI function to select model from dropdown \n", + "def trade(trading_function, model):\n", + " if model==\"GPT\":\n", + " yield from trade_gpt(trading_function)\n", + " elif model==\"Claude\":\n", + " yield from trade_claude(trading_function)\n", + " elif model==\"Gemini\":\n", + " yield from trade_gemini(trading_function)\n", + " elif model==\"CodeQwen\":\n", + " yield from trade_code_qwen(trading_function)\n", + " else:\n", + " raise ValueError(\"Unknown Model\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e6af1cd-f3d9-43f0-91d9-9800d9681a77", + "metadata": {}, + "outputs": [], + "source": [ + "# CSS styling for the UI\n", + "UI_CSS = \"\"\"\n", + "#title {\n", + " text-align: center;\n", + " font-size: 2.5em;\n", + " font-weight: bold;\n", + " margin-bottom: 10px;\n", + "}\n", + "\n", + "#description {\n", + " text-align: left;\n", + " font-size: 1.2em;\n", + " font-weight: bold;\n", + " margin-bottom: 20px;\n", + " color: #BBB;\n", + "}\n", + "\n", + "#simulate-btn {\n", + " height: 3em;\n", + " font-size: 2em !important;\n", + " padding: 12px 25px !important;\n", + " border-radius: 10px !important;\n", + " border: none !important;\n", + " cursor: pointer;\n", + " transition: background-color 0.3s, transform 0.2s; /* Smooth effects */\n", + "}\n", + "\n", + "#simulate-btn:hover {\n", + " background-color: #FFC107 !important; /* Bright golden-yellow on hover */\n", + " transform: scale(1.05); /* Slight zoom effect */\n", + " box-shadow: 0 6px 8px rgba(0, 0, 0, 0.25); /* Enhance shadow on hover */\n", + "}\n", + "\n", + "#simulate-btn:active {\n", + " background-color: #B8860B !important; /* Darker goldenrod on click */\n", + " transform: scale(0.95); /* Slight press effect */\n", + " box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); /* Reduce shadow on click */\n", + "}\n", + "\n", + "#simulate-btn,\n", + "#trading-decisions {\n", + " background-color: #DAA520 !important; /* Goldenrod color same as #simulate-btn */\n", + " color: #FFFFFF !important; /* White text for contrast */\n", + " box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); /* Subtle shadow for depth */\n", + "}\n", + "\n", + "#trading-decisions {\n", + " border: 2px solid #B8860B; /* Darker goldenrod border */\n", + "}\n", + "\n", + "#trading-decisions:focus {\n", + " outline: none;\n", + " box-shadow: 0 0 8px #FFC107; /* Bright golden-yellow glow on focus */\n", + "}\n", + "\n", + "#example-function, \n", + "#model-dropdown {\n", + " background-color: #007965 !important; /* Darker and sharper teal for better contrast */\n", + " color: #FFFFFF !important; /* Pure white for strong visibility */\n", + " cursor: pointer;\n", + " border: 2px solid #00594D; /* Deep teal border for emphasis */\n", + " box-shadow: 0 4px 8px rgba(0, 0, 0, 0.9); /* Strong shadow for depth */\n", + "}\n", + "\n", + "#example-function:focus,\n", + "#model-dropdown:focus {\n", + " outline: none;\n", + " box-shadow: 0 0 10px #00A389; /* Vibrant teal glow on focus */\n", + "}\n", + "\n", + "#model-dropdown:hover {\n", + " background-color: #005F4A !important; /* Darker teal for hover effect */\n", + " box-shadow: 0 6px 10px rgba(0, 0, 0, 0.4); /* Enhanced shadow on hover */\n", + " border-color: #00A389; /* Change border color for hover */\n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f733330f-6945-4be4-a2ab-9e68c94f70f0", + "metadata": {}, + "outputs": [], + "source": [ + "# Gradio UI\n", + "with gr.Blocks(css=UI_CSS) as ui:\n", + " # Title for the application\n", + " gr.Markdown(\"

🛠️ Trading Strategy Simulator

\")\n", + " \n", + " # Input and output section\n", + " with gr.Row():\n", + " trading_f = gr.Textbox(\n", + " label=\"📄 Trading Function Input\",\n", + " placeholder=\"Paste your trading function here...\",\n", + " lines=15,\n", + " value=TRADING_FUNCTION_EXAMPLE,\n", + " elem_id=\"example-function\"\n", + " )\n", + " decisions = gr.Textbox(\n", + " label=\"📊 Generated Trading Decisions\",\n", + " placeholder=\"Trading decisions will appear here...\",\n", + " lines=20,\n", + " interactive=False,\n", + " elem_id=\"trading-decisions\"\n", + " )\n", + " \n", + " with gr.Row():\n", + " # Dropdown scaled to take 1 part of the row\n", + " model = gr.Dropdown(\n", + " choices=MODELS,\n", + " label=\"🤖 Select AI Model\",\n", + " value=\"GPT\",\n", + " scale=1,\n", + " elem_id=\"model-dropdown\"\n", + " )\n", + " # Markdown for the description scaled to 2 parts of the row\n", + " with gr.Column(scale=2):\n", + " gr.Markdown(\n", + " \"\"\"\n", + "
\n", + " This interface allows you to test and simulate trading strategies using a predefined example function.\n", + " Simply input a trading function, select your preferred AI model, and see the generated trading decisions in action.
\n", + " Experiment with different strategies to refine your approach and analyze outcomes effectively.\n", + "
\n", + " \"\"\"\n", + " )\n", + " # Button scaled to take 1 part of the row\n", + " trade_btn = gr.Button(\n", + " \"💼 Simulate Trading\",\n", + " elem_id=\"simulate-btn\",\n", + " scale=1\n", + " )\n", + "\n", + " # Action button behavior\n", + " trade_btn.click(\n", + " fn=trade, \n", + " inputs=[trading_f, model], \n", + " outputs=[decisions]\n", + " )\n", + "\n", + "# Launch the UI in a browser\n", + "ui.launch(inbrowser=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d0ad093-425b-488e-8c3f-67f729dd9c06", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}