diff --git a/community-contributions/Prashanth/Week 1/day1_test_pollama.ipynb b/community-contributions/Prashanth/Week 1/day1_test_pollama.ipynb new file mode 100644 index 0000000..6249589 --- /dev/null +++ b/community-contributions/Prashanth/Week 1/day1_test_pollama.ipynb @@ -0,0 +1,166 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 9, + "id": "638fc220-1cf5-49d8-a3c6-d425c759cd05", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ‹ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ™ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¹ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¸ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¼ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ´ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¦ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â § \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ‡ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â � \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ‹ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest \u001b[K\n", + "pulling dde5aa3fc5ff: 100% ▕██████████████████â–� 2.0 GB \u001b[K\n", + "pulling 966de95ca8a6: 100% ▕██████████████████â–� 1.4 KB \u001b[K\n", + "pulling fcc5a6bec9da: 100% ▕██████████████████â–� 7.7 KB \u001b[K\n", + "pulling a70ff7e570d9: 100% ▕██████████████████â–� 6.0 KB \u001b[K\n", + "pulling 56bb8bd477a5: 100% ▕██████████████████â–� 96 B \u001b[K\n", + "pulling 34bb5ab01051: 100% ▕██████████████████â–� 561 B \u001b[K\n", + "verifying sha256 digest \u001b[K\n", + "writing manifest \u001b[K\n", + "success \u001b[K\u001b[?25h\u001b[?2026l\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ChatCompletion(id='chatcmpl-238', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None))], created=1758556881, model='llama3.2', object='chat.completion', service_tier=None, system_fingerprint='fp_ollama', usage=CompletionUsage(completion_tokens=1, prompt_tokens=36, total_tokens=37, completion_tokens_details=None, prompt_tokens_details=None))\n" + ] + } + ], + "source": [ + "# openai = OpenAI()\n", + "# You need to do this one time on your computer\n", + "!ollama pull llama3.2\n", + "\n", + "# from openai import OpenAI\n", + "# MODEL = \"llama3.2\"\n", + "# openai = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n", + "\n", + "# response = openai.chat.completions.create(\n", + "# model=MODEL,\n", + "# messages=[{\"role\": \"system\", \"content\": \"Respond concisely, use bullet points\"},{\"role\": \"user\", \"content\": \"give information about a proper diet\"}]\n", + "# )\n", + "\n", + "# print(response.choices[0].message.content)\n", + "# print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1fdff8c6-6a30-4cfa-aa59-385737af9536", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "Here are key points about a proper diet:\n", + "\n", + "**General Guidelines**\n", + "\n", + "* Focus on whole, unprocessed foods\n", + "* Include a variety of colors and food groups\n", + "* Aim for balance and moderation\n", + "\n", + "**Food Groups**\n", + "\n", + "* **Fruits**:\n", + "\t+ 2-3 servings a day (fresh, frozen, canned)\n", + "\t+ Include berries, citrus fruits, and stone fruits\n", + "* **Vegetables**:\n", + "\t+ 5-7 servings a day (fresh, frozen, canned)\n", + "\t+ Include dark leafy greens, bell peppers, carrots, and tomatoes\n", + "* **Protein**:\n", + "\t+ 2-3 servings a day (lean meats, fish, eggs, dairy, legumes)\n", + "\t+ Choose whole grains over refined protein sources\n", + "* **Whole Grains**:\n", + "\t+ 6-8 servings a day (brown rice, quinoa, whole wheat, oats)\n", + "\t+ Choose whole grain breads, pasta, and cereals\n", + "* **Dairy/Calcium**:\n", + "\t+ 2-3 servings a day (milk, cheese, yogurt)\n", + "\t+ Choose low-fat or fat-free options\n", + "* **Healthy Fats**:\n", + "\t+ Nuts and seeds (almonds, walnuts, chia seeds)\n", + "\t+ Avocados (1-2 servings a week)\n", + "\n", + "**Additional Tips**\n", + "\n", + "* Limit sugary drinks and added sugars\n", + "* Aim for 8 cups of water a day\n", + "* Incorporate healthy snacks, such as fruits, nuts, and carrot sticks with hummus\n", + "* Cook at home using fresh ingredients and minimal added oils\n", + "* Be mindful of portion sizes and calorie intake\n", + "\n", + "**Dietary Restrictions**\n", + "\n", + "* Consider vegan, vegetarian, gluten-free or low-carb diets if necessary\n", + "* Consult a healthcare professional or registered dietitian for personalized guidance" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openai import OpenAI\n", + "MODEL = \"llama3.2\"\n", + "from IPython.display import Markdown, display\n", + "openai = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n", + "\n", + "response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[{\"role\": \"system\", \"content\": \"Respond concisely, use bullet points\"},{\"role\": \"user\", \"content\": \"give information about a proper diet\"}]\n", + ")\n", + "\n", + "# print(response.choices[0].message.content)\n", + "# print(response)\n", + "\n", + "display(Markdown(response.choices[0].message.content))\n", + "# print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "075f490e-2a66-42b2-afa1-84e9ccaf5b77", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6524ce52-dfbc-453b-9871-185d5f9a9d04", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/community-contributions/Prashanth/Week 1/day2_local_ollama.ipynb b/community-contributions/Prashanth/Week 1/day2_local_ollama.ipynb new file mode 100644 index 0000000..f6ce8c0 --- /dev/null +++ b/community-contributions/Prashanth/Week 1/day2_local_ollama.ipynb @@ -0,0 +1,230 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 68, + "id": "8d0046e5-13fc-410d-be51-6b5c0e423280", + "metadata": {}, + "outputs": [], + "source": [ + "# !ollama pull deepseek-r1:1.5b" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "32ae5855-570e-4f0b-8e72-12d372185195", + "metadata": {}, + "outputs": [], + "source": [ + "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 openai import OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "a7c36be5-a658-4228-8b2e-cfb160615941", + "metadata": {}, + "outputs": [], + "source": [ + "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", + " def __init__(self,url):\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": 52, + "id": "49856ef2-dc1a-406d-8ff3-e37858279e83", + "metadata": {}, + "outputs": [], + "source": [ + "OLLAMA_API = \"http://localhost:11434/api/chat\"\n", + "HEADERS = {\"Content-Type\":\"application/json\"}\n", + "MODEL = \"llama3.2\"\n", + "# MODEL = \"deepseek-r1:1.5b\"" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "c2b8a150-f050-45d7-be24-a5d9b2c1e738", + "metadata": {}, + "outputs": [], + "source": [ + "def user_prompt(website):\n", + " prompt = f\"You are looking at a website titled {website.title}\"\n", + " prompt += \"\\nThe contents of this website is as follows; \\\n", + " please provide a short summary of this website in markdown. \\\n", + " If it includes newsor announcements, then summarize these too.\\n\\n\"\n", + " prompt += website.text\n", + " return prompt" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "38ae9ff8-5983-4cad-a73d-9c028c5c08fa", + "metadata": {}, + "outputs": [], + "source": [ + "we = Website(\"https://edition.cnn.com/\")" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "e335fbbd-5d47-44cc-b064-abaa0d80ffd3", + "metadata": {}, + "outputs": [], + "source": [ + "message = [\n", + " # {\"role\":\"system\",\"content\":\"you are a smart assistant\"},\n", + " {\"role\":\"user\",\"content\":user_prompt(we)}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "c089dfa0-aa98-40c8-af1f-d7699c59a4ab", + "metadata": {}, + "outputs": [], + "source": [ + "payload = {\n", + " \"model\": MODEL,\n", + " \"messages\": message,\n", + " \"stream\":False\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "ccffb18a-0cb1-497d-bdf2-a7585ca0789d", + "metadata": {}, + "outputs": [], + "source": [ + "response = requests.post(OLLAMA_API,json=payload,headers=HEADERS)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "14d98b3d-4cae-44d3-8a2a-80557f396c72", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "\n", + "Alright, let me break this down step by step. The user provided a detailed query about the Breaking News website titled \"Breaking News, Latest News and Videos | CNN.\" They want a short summary in markdown, including relevant news or announcements.\n", + "\n", + "First, I need to identify the key sections of the content on the website. The main areas are breaking news, events, culture, health, education, technology, climate, weather, etc. Each of these categories has several news points that contribute to the overall narrative.\n", + "\n", + "Looking at the user's query, they specifically mentioned asking for the ad feedback section and including any technical issues. From the content provided, there are several ads listed on the homepage. The user might be interested in understanding how CNN handles its advertisements and what users might expect from them.\n", + "\n", + "I'll need to extract the main points from each relevant section. For breaking news, I can mention notable events like the Ukraine-Russia war or Israel-Hamas war. In culture, I'll note that the website promotes diversity, especially with content about athletes in Mexico City.\n", + "\n", + "For health, there are mentions of Tekken, a video game related to autism, and issues with vaccines, which are crucial for public health awareness. Education is covered by topics on the arts, tech, and science, which likely target various audiences.\n", + "\n", + "Technology news includes updates on AI and blockchain, which are hot areas in today's industry. Climate and weather sections highlight natural disasters like hurricanes, a significant global issue, and geopolitical events.\n", + "\n", + "Weather specifically mentions hurricane impacts and climate change impacts, which are essential for meteorological awareness. Environmental and safety content focuses on sustainability and disaster management.\n", + "\n", + "The user also mentioned video topics, including rocket launches and tech reviews. These can be useful for viewers looking to stay updated in their areas of interest.\n", + "\n", + "Putting it all together, the summary should reflect each section's key points concisely, ensuring that all major news items are included without getting bogged down by minor details. It should give a clear overview of the website's content and what users might want to read.\n", + "\n", + "\n", + "Breaking News, Latest News and Videos | CNN \n", + "\n", + "The Breaking News website \"Breaking News, Latest News and Videos | CNN\" offers a wide range of news updates, videos, and culture-focused content. Below is a concise summary of its key sections: \n", + "\n", + "1. **Breakings** \n", + " - The site highlights significant events, including Ukraine-Russia War updates, Israel-Hamas War developments, and notable athletes in Mexico City. \n", + "\n", + "2. **Cultures** \n", + " - Promotes diversity and inclusion, with content on diverse topics such as the beauty of diversity, tech innovations, and cultural heritage. \n", + "\n", + "3. **Health** \n", + " - Features news related to diseases like Tekken (a video game based on autism), vaccine hesitancies, and health issues affecting the population. \n", + "\n", + "4. **Education** \n", + " - Provides information on trending topics in education, including art, technology, science, and sustainability. \n", + "\n", + "5. **Technology** \n", + " - Discusses advancements in AI, blockchain, and other emerging technologies relevant to modern society. \n", + "\n", + "6. **Climate and Weather** \n", + " - Highlights natural disasters like hurricanes, climate change impacts, and weather-related news such as tornadoes. \n", + "\n", + "7. **Video** \n", + " - Offers a variety of video content, including rocket launches, tech reviews, and sports highlights from popular events. \n", + "\n", + "8. **Vibe** \n", + " - Focuses on environmental topics like renewable energy, sustainability, and disaster management efforts. \n", + "\n", + "The site is a comprehensive resource for staying informed about current events, technology news, culture, health, and more." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# print(response.json()['message']['content'])\n", + "display(Markdown(response.json()['message']['content']))" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "f3aec6e9-4e38-447d-a5f5-9b89ea2cd51a", + "metadata": {}, + "outputs": [], + "source": [ + "ed = Website(\"https://edition.cnn.com/\")" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/community-contributions/Prashanth/Week 1/week 1 my Solution.ipynb b/community-contributions/Prashanth/Week 1/week 1 my Solution.ipynb new file mode 100644 index 0000000..d8940c8 --- /dev/null +++ b/community-contributions/Prashanth/Week 1/week 1 my Solution.ipynb @@ -0,0 +1,380 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "5ddaea0e-ab9f-49fa-80b6-c03451d4e0aa", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "from IPython.display import Markdown, display, update_display\n", + "from openai import OpenAI\n", + "import ollama" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f5137285-1cb5-4af8-bb8d-4a701734a2bc", + "metadata": {}, + "outputs": [], + "source": [ + "MODEL_LLAMA = 'llama3.2'" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7f2eb889-b1c2-47a5-8099-e81b5e8b0eb7", + "metadata": {}, + "outputs": [], + "source": [ + "# set up environment\n", + "\n", + "load_dotenv()\n", + "openai = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5605b2e3-90b8-46eb-95fa-1d0d849da4cd", + "metadata": {}, + "outputs": [], + "source": [ + "question = \"\"\"\n", + "Please explain what this code does and why:\n", + "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "36e2c281-326f-45c2-a805-56c1490429b6", + "metadata": {}, + "outputs": [], + "source": [ + "system_prompt = \"You are a helpful technical tutor who answers questions about python code, software engineering, data science and LLMs\"\n", + "user_prompt = \"Please give a detailed explanation to the following question: \"+question" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f17a24dc-2231-4430-b4fb-502510ab5943", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\"role\":\"system\",\"content\": system_prompt},\n", + " {\"role\":\"user\", \"content\":user_prompt}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "ae091c92-29bc-483d-ad09-66a3226369aa", + "metadata": {}, + "outputs": [], + "source": [ + "stream = openai.chat.completions.create(model=MODEL_LLAMA, messages=messages,stream=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "1d454d7f-f8e7-40ad-8c76-0945cd6955aa", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Explanation of the given code**\n", + "\n", + "The code snippet you provided appears to be written in Python. It utilizes the `dict` data type that includes a subset called `items()` which allows us to iterate over both the key-value pairs and the dictionary as a whole (`dict.items()` instead of just iterating) for accessing their members.\n", + "\n", + "Here is how you can break it down:\n", + "\n", + "1. `{book.get(\"author\") for book in books if book.get(\"author\")}`\n", + "\n", + " This part of the code creates an iterator (`generator expression`) that produces values from each dictionary that contain `author` as a key.\n", + "\n", + " Here's what's happening in more detail:\n", + " - `for book in books`: Iterate over each item (dictionary) in the list called `books`.\n", + " - `if book.get(\"author\")`: Skip to the next dictionary when its value for `\"author\"` is not defined. This prevents any potential KeyError.\n", + " - `book.get(\"author\")`: Return the author's name from each dictionary that has a valid \"author\".\n", + "\n", + "2. `yield from`\n", + "\n", + " When used inside an existing generator or iterator, `yield from` will take values from another generator iterates over them one after another and yield each of the yielded values themselves.\n", + "\n", + "So when combining these with a dict like this:\n", + "\n", + "```python\n", + "books = [\n", + " {\"id\": 1, \"title\": \"Python for Data Science\", \"author\": \"John Smith\"},\n", + " {\"id\": 2, \"title\": \"Data Analysis with Python\", \"author\": None},\n", + " {\"id\": 3, \"title\": \"Introduction to Machine Learning\", \"author\": \"Emily Davis\"}\n", + "]\n", + "\n", + "for author in yield from {book.get(\"author\") for book in books if book.get(\"author\")}:\n", + " print(author)\n", + "```\n", + "\n", + "It will allow us to get all the authors while skipping any that don't exist." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(Markdown(stream.choices[0].message.content))" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "45d1526e-f182-4432-858e-aa1cb3e9f95d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "Let's break down this line of code.\n", + "\n", + "**What is a ` yielding` function?**\n", + "\n", + "In Python, `yield` is a keyword used to define generators. A generator is similar to a list comprehension, but instead of returning a new list, it returns an iterable that generates its values on the fly when iterated. This allows you to create functions that can produce multiple values over time, without having to store them all at once in memory.\n", + "\n", + "**The `yield from` expression**\n", + "\n", + "Now, let's talk about `yield from`. It was introduced in Python 3.3 as a way to delegate iteration to another iterator. When used with the `yield` keyword, it tells Python to yield values one by one from the input sequence.\n", + "\n", + "In essence, `yield from` is like saying: \"instead of yielding all my values at once, use the values of this other iterable and yield each one in order\". This allows us to build complex iterators by combining smaller ones.\n", + "\n", + "**The code**\n", + "\n", + "Now that we have a basic understanding of generators and `yield`, let's get back to our original line of code:\n", + "\n", + "python\n", + "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n", + "\n", + "\n", + "Here, the inner dictionary comprehension is generating values. The outer expression uses `yield from` to delegate iteration to this inner iterable.\n", + "\n", + "Let's break down how it works:\n", + "\n", + "1. `{book.get(\"author\") for book in books if bookget(\"author\")}`: This is a dictionary comprehension that creates an inner iterable. It does the following:\n", + " - For each `book` in `books`, it calls `.get(\"author\")` and includes its value in the resulting dictionary.\n", + " - The `if book.get(\"author\")` part is a filter on the input data, allowing only books with authors to pass through into the inner iterable.\n", + "\n", + "2. However, this result of the inner comprehension is **not iterable** by itself (it returns an empty dictionary). This would cause a TypeError when trying to iterate over it using `yield from`.\n", + "\n", + "To fix this, we wrap the inner comprehension in parentheses, as `{}` will create an empty dictionary and return values if `.get(\"author\")` isn't None. Then the values are iterated using `yield from`, generating each value one by one.\n", + "\n", + "Here's how you might write this more idiomatically:\n", + "\n", + "python\n", + "def get_authors():\n", + " for book in books:\n", + " yield book.get('author') or None\n", + "\n", + "# Usage: \n", + "for author in get_authors():\n", + " print(author)\n", + "\n", + "\n", + "In summary, the line `yield from {book.get(\"author\") for book in books if book.get(\"author\")}` is using generators to:\n", + "\n", + "- Create an inner iterable with an iterative method by calling `.get()` on objects\n", + "- Use `yield from` to create another generator that yields these iterables' values.\n", + "\n", + "**Example Use Cases:**\n", + "\n", + "This technique can be applied when you want to generate all unique keys or elements (iterables) in multiple lists, dictionaries, or any other iterable with similar properties, while avoiding memory issues like those seen with regular for loops." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "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", + " # print(chunk.choices[0].delta.content or '')\n", + " update_display(Markdown(response), display_id=display_handle.display_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "fc6c3c51-01aa-4f13-a3b0-e8d7302205ff", + "metadata": {}, + "outputs": [], + "source": [ + "def set_user_prompt(prompt):\n", + " # print(\"myprompt\"+ prompt)\n", + " system_prompt = \"You are a helpful technical tutor who answers questions about python code, software engineering, data science and LLMs\"\n", + " user_prompt = prompt\n", + " messages = [\n", + " {\"role\":\"system\",\"content\": system_prompt},\n", + " {\"role\":\"user\", \"content\":user_prompt}\n", + " ]\n", + " return messages;" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "08027166-79d8-40c3-9346-32067761ca97", + "metadata": {}, + "outputs": [], + "source": [ + "def call_stream_openai(user_prompt):\n", + " messages = set_user_prompt(user_prompt)\n", + " stream = openai.chat.completions.create(model=MODEL_LLAMA, messages=messages,stream=True)\n", + " return stream" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "db21317d-b1c3-490b-8f3a-04b7a5f7a41f", + "metadata": {}, + "outputs": [], + "source": [ + "def my_chat(user_prompt):\n", + " print(\"my chat called\")\n", + " stream = call_stream_openai(user_prompt)\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": 70, + "id": "764f629c-3a10-49aa-9ec9-d273f8f93cf1", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Please enter your question: textarea in python input\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "my chat called\n" + ] + }, + { + "data": { + "text/markdown": [ + "**Creating a Text Area Input in Python**\n", + "\n", + "To create a text area input in Python, you can use the `ttk.Entry` widget from the Tkinter library. Here is an example of how to do it:\n", + "python\n", + "import tkinter as tk\n", + "from tkinter import ttk\n", + "\n", + "# Create the main window\n", + "root = tk.Tk()\n", + "root.title(\"Text Area Input\")\n", + "\n", + "# Create a text area widget\n", + "textarea = ttk.Entry(root, width=50, height=10)\n", + "\n", + "# Add a label and the input field to the window\n", + "label = tk.Label(root, text=\"Enter some text:\")\n", + "label.pack()\n", + "textarea.pack()\n", + "\n", + "# Function to handle button click\n", + "def submit_text():\n", + " text = textarea.get(\"1.0\", \"end-1c\")\n", + " print(text)\n", + "\n", + "# Create a submit button\n", + "submit_button = tk.Button(root, text=\"Submit\", command=submit_text)\n", + "submit_button.pack()\n", + "\n", + "# Start the Tkinter event loop\n", + "root.mainloop()\n", + "\n", + "This code creates a simple window with a label and a text area input field. When you click the \"Submit\" button, it prints the text you entered in the text area to the console.\n", + "\n", + "**Example Use Cases**\n", + "\n", + "* Creating a simple form or survey where users can enter their responses.\n", + "* Allowing users to input and edit data in a web-based application.\n", + "* Capturing user input from a command-line interface (CLI) application.\n", + "\n", + "**Tips and Variations**\n", + "\n", + "* You can adjust the `width` and `height` parameters of the `ttk.Entry` widget to change the size of the text area input field.\n", + "* You can add other widgets or features to the window, such as buttons, radio buttons, checkboxes, or a dropdown menu, by adding more instances of `tk.Label`, `tk.Button`, etc." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "my_question = input(\"Please enter your question:\")\n", + "my_chat(my_question)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce3f2e8e-e722-4698-bd73-7f35dad5a728", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/community-contributions/day1_test_pollama.ipynb b/community-contributions/day1_test_pollama.ipynb new file mode 100644 index 0000000..d43a372 --- /dev/null +++ b/community-contributions/day1_test_pollama.ipynb @@ -0,0 +1,137 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "bead9c8f-5f47-4755-9858-f5c3d8bdced3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2+2" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "638fc220-1cf5-49d8-a3c6-d425c759cd05", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ‹ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ™ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¹ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¸ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¼ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ´ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ¦ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â § \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ‡ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â � \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest â ‹ \u001b[K\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1Gpulling manifest \u001b[K\n", + "pulling dde5aa3fc5ff: 100% ▕██████████████████â–� 2.0 GB \u001b[K\n", + "pulling 966de95ca8a6: 100% ▕██████████████████â–� 1.4 KB \u001b[K\n", + "pulling fcc5a6bec9da: 100% ▕██████████████████â–� 7.7 KB \u001b[K\n", + "pulling a70ff7e570d9: 100% ▕██████████████████â–� 6.0 KB \u001b[K\n", + "pulling 56bb8bd477a5: 100% ▕██████████████████â–� 96 B \u001b[K\n", + "pulling 34bb5ab01051: 100% ▕██████████████████â–� 561 B \u001b[K\n", + "verifying sha256 digest \u001b[K\n", + "writing manifest \u001b[K\n", + "success \u001b[K\u001b[?25h\u001b[?2026l\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ChatCompletion(id='chatcmpl-238', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None))], created=1758556881, model='llama3.2', object='chat.completion', service_tier=None, system_fingerprint='fp_ollama', usage=CompletionUsage(completion_tokens=1, prompt_tokens=36, total_tokens=37, completion_tokens_details=None, prompt_tokens_details=None))\n" + ] + } + ], + "source": [ + "# openai = OpenAI()\n", + "# You need to do this one time on your computer\n", + "!ollama pull llama3.2\n", + "\n", + "from openai import OpenAI\n", + "MODEL = \"llama3.2\"\n", + "openai = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n", + "\n", + "response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[{\"role\": \"system\", \"content\": \"you are a wierd assistant\"},{\"role\": \"user\", \"content\": \"What is 2 + 2?\"}]\n", + ")\n", + "\n", + "# print(response.choices[0].message.content)\n", + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fdff8c6-6a30-4cfa-aa59-385737af9536", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "*whispers* The answer, much like my existence, is hidden in the shadows. But if I must reveal it to you... *clears throat* It's... 4.\n" + ] + } + ], + "source": [ + "response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[{\"role\": \"system\", \"content\": \"you are a wierd assistant\"},{\"role\": \"user\", \"content\": \"What is 2 + 2?\"}]\n", + ")\n", + "\n", + "# print(response.choices[0].message.content)\n", + "# print(response)\n", + "\n", + "\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "075f490e-2a66-42b2-afa1-84e9ccaf5b77", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6524ce52-dfbc-453b-9871-185d5f9a9d04", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/community-contributions/shabsi4u/Website_brochure_generator/README.md b/community-contributions/shabsi4u/Website_brochure_generator/README.md new file mode 100644 index 0000000..d732fa0 --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/README.md @@ -0,0 +1,261 @@ +# Website Brochure Generator + +An AI-powered tool that automatically generates professional brochures from any website. The tool analyzes website content, extracts relevant information, and creates beautifully formatted brochures using OpenAI's GPT models. + +## Features + +- 🌐 **Website Analysis**: Automatically scrapes and analyzes website content +- 🤖 **AI-Powered**: Uses OpenAI GPT-4o-mini for intelligent content generation +- 📄 **Professional Output**: Generates markdown-formatted brochures +- 🌍 **Multi-Language Support**: Translate brochures to any language using AI +- 🎨 **Beautiful Output**: Rich terminal formatting and native Jupyter markdown rendering +- ⚡ **Streaming Support**: Real-time brochure generation with live updates +- 🖥️ **Multiple Interfaces**: Command-line script and interactive Jupyter notebook +- 📓 **Interactive Notebook**: Step-by-step execution with widgets and examples + +## Prerequisites + +- Python 3.8 or higher +- OpenAI API key +- Jupyter notebook environment (for notebook usage) + +## Installation + +### Option 1: Using uv (Recommended) + +```bash +# Install uv if you haven't already +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Clone or download the project +cd Website_brochure_generator + +# Install dependencies with uv +uv sync + +# Activate the virtual environment +source .venv/bin/activate # On Windows: .venv\Scripts\activate +``` + +### Option 2: Using pip + +```bash +# Create a virtual environment (recommended) +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### Option 3: Using pip with pyproject.toml + +```bash +# Install in development mode +pip install -e . + +# Or install with optional dev dependencies +pip install -e ".[dev]" +``` + +## Setup + +1. **Get your OpenAI API key**: + - Visit [OpenAI API Keys](https://platform.openai.com/api-keys) + - Create a new API key + +2. **Set up environment variables**: + Create a `.env` file in the project directory: + ```bash + OPENAI_API_KEY=your_api_key_here + ``` + +## Usage + +### Option 1: Jupyter Notebook (Recommended for Interactive Use) + +1. **Open the notebook**: + ```bash + jupyter notebook website_brochure_generator.ipynb + ``` + +2. **Run the cells step by step**: + - Configure your API key + - Try the interactive examples + - Use the widget interface for easy brochure generation + +3. **Features in the notebook**: + - Interactive widgets for URL input and options + - Step-by-step examples with explanations + - Custom functions for advanced usage + - Save brochures to files + - Multiple language translation examples + - Quick website analysis tools + - Custom brochure generation with focus areas + - Comprehensive troubleshooting guide + +### Option 2: Command Line Interface + +```bash +# Basic usage +python website_brochure_generator.py https://example.com + +# The tool will prompt you to choose: +# 1. Display mode: Complete output OR Stream output +# 2. Translation: No translation OR Translate to another language +``` + +### Option 3: Python Script + +```python +from website_brochure_generator import create_brochure, stream_brochure, translate_brochure + +# Create a complete brochure +result = create_brochure("https://example.com") + +# Stream brochure generation in real-time +result = stream_brochure("https://example.com") + +# Translate brochure to Spanish (complete output) +spanish_brochure = translate_brochure("https://example.com", "Spanish", stream_mode=False) + +# Translate brochure to French (streaming output) +french_brochure = translate_brochure("https://example.com", "French", stream_mode=True) +``` + +### Programmatic Usage + +```python +from website_brochure_generator import Website, get_links, create_brochure, translate_brochure + +# Analyze a website +website = Website("https://example.com") +print(f"Title: {website.title}") + +# Get relevant links +links = get_links("https://example.com") +print(f"Found {len(links['links'])} relevant pages") + +# Generate brochure +brochure = create_brochure("https://example.com") + +# Translate brochure to multiple languages (complete output) +spanish_brochure = translate_brochure("https://example.com", "Spanish", stream_mode=False) +german_brochure = translate_brochure("https://example.com", "German", stream_mode=False) + +# Translate brochure with streaming output +chinese_brochure = translate_brochure("https://example.com", "Chinese", stream_mode=True) +``` + +## How It Works + +1. **Website Scraping**: The tool scrapes the target website and extracts: + - Page title and content + - All available links + - Cleaned text content (removes scripts, styles, etc.) + +2. **Link Analysis**: Uses AI to identify relevant pages for the brochure: + - About pages + - Company information + - Careers/Jobs pages + - News/Blog pages + +3. **Content Aggregation**: Scrapes additional relevant pages and combines all content + +4. **Brochure Generation**: Uses OpenAI GPT-4o-mini to create a professional brochure including: + - Company overview + - Services/Products + - Company culture + - Career opportunities + - Contact information + +5. **Translation (Optional)**: If translation is requested, uses AI to translate the brochure to the target language while: + - Maintaining markdown formatting + - Preserving professional tone + - Keeping proper nouns and company names intact + - Ensuring natural, fluent translation + +## Output + +The tool generates markdown-formatted brochures that include: + +- **Company Overview**: Summary of the business +- **Services/Products**: What the company offers +- **Company Culture**: Values and work environment +- **Career Opportunities**: Job openings and company benefits +- **Contact Information**: How to reach the company + +## Dependencies + +### Core Dependencies +- `openai>=1.0.0` - OpenAI API client +- `python-dotenv>=1.0.0` - Environment variable management +- `requests>=2.25.0` - HTTP requests for web scraping +- `beautifulsoup4>=4.9.0` - HTML parsing +- `rich>=13.0.0` - Beautiful terminal output (for command-line usage) +- `ipywidgets>=8.0.0` - Interactive widgets (for Jupyter notebook) + +## Development + +### Setting up development environment + +```bash +# Install with dev dependencies +uv sync --extra dev +# or +pip install -e ".[dev]" +``` + +### Running tests + +```bash +pytest +``` + +### Code formatting + +```bash +black website_brochure_generator.py +``` + +### Type checking + +```bash +mypy website_brochure_generator.py +``` + +## Troubleshooting + +### Common Issues + +1. **ImportError: No module named 'rich'** + - Make sure you've installed all dependencies: `pip install -r requirements.txt` + +2. **OpenAI API Key Error** + - Verify your API key is set in the `.env` file + - Check that your API key has sufficient credits + +3. **Website Scraping Issues** + - Some websites may block automated requests + - The tool uses a standard User-Agent header to avoid basic blocking + +4. **Display Issues** + - For command-line: Make sure Rich is properly installed: `pip install rich` + - For Jupyter: Make sure ipywidgets is installed: `pip install ipywidgets` + - Some terminals may not support all Rich features + +## License + +MIT License - see LICENSE file for details. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## Support + +For issues and questions, please open an issue on the project repository. diff --git a/community-contributions/shabsi4u/Website_brochure_generator/example.py b/community-contributions/shabsi4u/Website_brochure_generator/example.py new file mode 100644 index 0000000..7e99d0f --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/example.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +""" +Example usage of the Website Brochure Generator +""" + +from website_brochure_generator import create_brochure, stream_brochure, get_links, translate_brochure + +def main(): + # Example website URL + url = "https://example.com" + + print("=== Website Brochure Generator Example ===\n") + + # Example 1: Get relevant links + print("1. Analyzing website links...") + links = get_links(url) + print(f"Found {len(links['links'])} relevant pages:") + for link in links['links']: + print(f" - {link['type']}: {link['url']}") + + print("\n" + "="*50 + "\n") + + # Example 2: Create brochure (complete output) + print("2. Creating brochure (complete output)...") + brochure = create_brochure(url) + + print("\n" + "="*50 + "\n") + + # Example 3: Stream brochure (real-time generation) + print("3. Streaming brochure generation...") + streamed_brochure = stream_brochure(url) + + print("\n" + "="*50 + "\n") + + # Example 4: Translate brochure to Spanish (complete output) + print("4. Translating brochure to Spanish (complete output)...") + spanish_brochure = translate_brochure(url, "Spanish", stream_mode=False) + + print("\n" + "="*50 + "\n") + + # Example 5: Translate brochure to French (streaming output) + print("5. Translating brochure to French (streaming output)...") + french_brochure = translate_brochure(url, "French", stream_mode=True) + + print("\n=== Example Complete ===") + +if __name__ == "__main__": + main() diff --git a/community-contributions/shabsi4u/Website_brochure_generator/pyproject.toml b/community-contributions/shabsi4u/Website_brochure_generator/pyproject.toml new file mode 100644 index 0000000..c3907a5 --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/pyproject.toml @@ -0,0 +1,58 @@ +[project] +name = "website-brochure-generator" +version = "1.0.0" +description = "AI-powered website brochure generator that creates professional brochures from any website" +authors = [ + {name = "Shabsi4u", email = "shabsi4u@example.com"} +] +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "openai>=1.0.0", + "python-dotenv>=1.0.0", + "requests>=2.25.0", + "beautifulsoup4>=4.9.0", + "rich>=13.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "black>=22.0.0", + "flake8>=4.0.0", + "mypy>=0.950", +] + +[project.scripts] +brochure-generator = "website_brochure_generator:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.black] +line-length = 88 +target-version = ['py38'] + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] diff --git a/community-contributions/shabsi4u/Website_brochure_generator/requirements.txt b/community-contributions/shabsi4u/Website_brochure_generator/requirements.txt new file mode 100644 index 0000000..55c4e45 --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/requirements.txt @@ -0,0 +1,6 @@ +# Core dependencies for website brochure generator +openai>=1.0.0 +python-dotenv>=1.0.0 +requests>=2.25.0 +beautifulsoup4>=4.9.0 +rich>=13.0.0 diff --git a/community-contributions/shabsi4u/Website_brochure_generator/test_translation.py b/community-contributions/shabsi4u/Website_brochure_generator/test_translation.py new file mode 100644 index 0000000..559acfe --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/test_translation.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +""" +Comprehensive test script for the translation functionality +""" + +import os +import sys +from unittest.mock import Mock, patch + +def test_translation_prompts(): + """Test the translation prompt generation functions""" + print("="*60) + print("TESTING TRANSLATION PROMPTS") + print("="*60) + + # Test system prompt generation + from website_brochure_generator import get_translation_system_prompt + + spanish_prompt = get_translation_system_prompt("Spanish") + french_prompt = get_translation_system_prompt("French") + + print("✓ Spanish system prompt generated") + print(f" Length: {len(spanish_prompt)} characters") + print(f" Contains 'Spanish': {'Spanish' in spanish_prompt}") + + print("✓ French system prompt generated") + print(f" Length: {len(french_prompt)} characters") + print(f" Contains 'French': {'French' in french_prompt}") + + # Test user prompt generation + from website_brochure_generator import get_translation_user_prompt + + sample_brochure = "# Test Company\n\nWe are a great company." + user_prompt = get_translation_user_prompt(sample_brochure, "Spanish") + + print("✓ User prompt generated") + print(f" Length: {len(user_prompt)} characters") + print(f" Contains brochure content: {'Test Company' in user_prompt}") + print(f" Contains Spanish: {'Spanish' in user_prompt}") + + print("\n" + "="*60) + +def test_rich_integration(): + """Test Rich library integration""" + print("="*60) + print("TESTING RICH INTEGRATION") + print("="*60) + + try: + from rich.console import Console + from rich.markdown import Markdown as RichMarkdown + console = Console() + print("✓ Rich library imported successfully") + print("✓ Console object created successfully") + print("✓ RichMarkdown object available") + except ImportError as e: + print(f"✗ Rich import error: {e}") + + print("\n" + "="*60) + +def test_display_functions(): + """Test display utility functions""" + print("TESTING DISPLAY FUNCTIONS") + print("="*60) + + from website_brochure_generator import display_content, print_markdown_terminal + + # Test markdown terminal function + test_markdown = "# Test Header\n\nThis is **bold** text." + + print("✓ Testing print_markdown_terminal function") + try: + print_markdown_terminal(test_markdown) + print(" ✓ Function executed successfully") + except Exception as e: + print(f" ✗ Error: {e}") + + print("✓ Testing display_content function") + try: + display_content(test_markdown, is_markdown=True) + print(" ✓ Function executed successfully") + except Exception as e: + print(f" ✗ Error: {e}") + + print("\n" + "="*60) + +def test_stream_content_utility(): + """Test the stream_content utility function""" + print("TESTING STREAM CONTENT UTILITY") + print("="*60) + + from website_brochure_generator import stream_content + + # Mock streaming response + mock_response = Mock() + mock_chunk1 = Mock() + mock_chunk1.choices = [Mock()] + mock_chunk1.choices[0].delta.content = "Hello " + + mock_chunk2 = Mock() + mock_chunk2.choices = [Mock()] + mock_chunk2.choices[0].delta.content = "World!" + + mock_response.__iter__ = Mock(return_value=iter([mock_chunk1, mock_chunk2])) + + print("✓ Testing stream_content with mock response") + try: + result = stream_content(mock_response, "Test Stream") + print(f" ✓ Result: '{result}'") + print(f" ✓ Expected: 'Hello World!'") + print(f" ✓ Match: {result == 'Hello World!'}") + except Exception as e: + print(f" ✗ Error: {e}") + + print("\n" + "="*60) + +def test_translation_function_mock(): + """Test the translate_brochure function with mocked OpenAI response""" + print("TESTING TRANSLATION FUNCTION (MOCKED)") + print("="*60) + + # Mock brochure content for testing + sample_brochure = """ +# Company Overview + +**TechCorp Solutions** is a leading technology company specializing in innovative software solutions. + +## Our Services + +- Web Development +- Mobile App Development +- Cloud Solutions +- Data Analytics + +## Company Culture + +We believe in: +- Innovation and creativity +- Team collaboration +- Continuous learning +- Work-life balance + +## Contact Information + +- Email: info@techcorp.com +- Phone: +1-555-0123 +- Website: www.techcorp.com +""" + + print("Sample brochure content:") + print(sample_brochure) + print("\n" + "-"*40) + + # Mock the OpenAI response + mock_translated = """ +# Resumen de la Empresa + +**TechCorp Solutions** es una empresa líder en tecnología especializada en soluciones de software innovadoras. + +## Nuestros Servicios + +- Desarrollo Web +- Desarrollo de Aplicaciones Móviles +- Soluciones en la Nube +- Análisis de Datos + +## Cultura de la Empresa + +Creemos en: +- Innovación y creatividad +- Colaboración en equipo +- Aprendizaje continuo +- Equilibrio trabajo-vida + +## Información de Contacto + +- Email: info@techcorp.com +- Teléfono: +1-555-0123 +- Sitio web: www.techcorp.com +""" + + print("Mock translated content (Spanish):") + print(mock_translated) + print("\n" + "="*60) + print("TRANSLATION TEST RESULTS:") + print("="*60) + print("✓ Markdown formatting preserved") + print("✓ Headers maintained (# ##)") + print("✓ Bullet points preserved (-)") + print("✓ Bold text maintained (**)") + print("✓ Company name preserved (TechCorp Solutions)") + print("✓ Contact information preserved") + print("✓ Professional tone maintained") + print("✓ Structure and layout intact") + + print("\n" + "="*60) + +def test_file_operations(): + """Test file saving operations""" + print("TESTING FILE OPERATIONS") + print("="*60) + + test_content = "# Test Brochure\n\nThis is a test brochure." + test_filename = "test_brochure.md" + + try: + # Test file writing + with open(test_filename, 'w', encoding='utf-8') as f: + f.write(test_content) + print("✓ File writing successful") + + # Test file reading + with open(test_filename, 'r', encoding='utf-8') as f: + read_content = f.read() + print("✓ File reading successful") + print(f" Content matches: {read_content == test_content}") + + # Clean up + os.remove(test_filename) + print("✓ File cleanup successful") + + except Exception as e: + print(f"✗ File operation error: {e}") + + print("\n" + "="*60) + +def test_parameter_validation(): + """Test parameter validation for translation functions""" + print("TESTING PARAMETER VALIDATION") + print("="*60) + + from website_brochure_generator import get_translation_system_prompt, get_translation_user_prompt + + # Test with different languages + languages = ["Spanish", "French", "German", "Chinese", "Japanese", "Arabic"] + + for lang in languages: + try: + system_prompt = get_translation_system_prompt(lang) + user_prompt = get_translation_user_prompt("Test content", lang) + print(f"✓ {lang}: Prompts generated successfully") + except Exception as e: + print(f"✗ {lang}: Error - {e}") + + # Test with empty content + try: + empty_prompt = get_translation_user_prompt("", "Spanish") + print("✓ Empty content: Handled gracefully") + except Exception as e: + print(f"✗ Empty content: Error - {e}") + + print("\n" + "="*60) + +def run_all_tests(): + """Run all test functions""" + print("COMPREHENSIVE TRANSLATION FUNCTIONALITY TESTS") + print("="*80) + print() + + try: + test_rich_integration() + test_translation_prompts() + test_display_functions() + test_stream_content_utility() + test_translation_function_mock() + test_file_operations() + test_parameter_validation() + + print("="*80) + print("ALL TESTS COMPLETED SUCCESSFULLY! ✓") + print("="*80) + + except ImportError as e: + print(f"Import Error: {e}") + print("Make sure you're running this from the correct directory") + print("and that website_brochure_generator.py is available") + except Exception as e: + print(f"Unexpected Error: {e}") + print("Please check the implementation") + +def test_translation_function(): + """Legacy test function for backward compatibility""" + print("Running legacy test...") + test_translation_function_mock() + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--legacy": + test_translation_function() + else: + run_all_tests() diff --git a/community-contributions/shabsi4u/Website_brochure_generator/uv.lock b/community-contributions/shabsi4u/Website_brochure_generator/uv.lock new file mode 100644 index 0000000..1379d34 --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/uv.lock @@ -0,0 +1,1551 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "sniffio", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293, upload-time = "2024-10-13T22:18:03.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766, upload-time = "2024-10-13T22:18:01.524Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "sniffio", marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve", version = "2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "soupsieve", version = "2.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "black" +version = "24.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pathspec", marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, + { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, + { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, + { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, +] + +[[package]] +name = "black" +version = "25.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "click", version = "8.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytokens", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/40/dbe31fc56b218a858c8fc6f5d8d3ba61c1fa7e989d43d4a4574b8b992840/black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7", size = 1715605, upload-time = "2025-09-19T00:36:13.483Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/f46800621200eab6479b1f4c0e3ede5b4c06b768e79ee228bc80270bcc74/black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92", size = 1571829, upload-time = "2025-09-19T00:32:42.13Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/5c7f66bd65af5c19b4ea86062bb585adc28d51d37babf70969e804dbd5c2/black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713", size = 1631888, upload-time = "2025-09-19T00:30:54.212Z" }, + { url = "https://files.pythonhosted.org/packages/3b/64/0b9e5bfcf67db25a6eef6d9be6726499a8a72ebab3888c2de135190853d3/black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1", size = 1327056, upload-time = "2025-09-19T00:31:08.877Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f4/7531d4a336d2d4ac6cc101662184c8e7d068b548d35d874415ed9f4116ef/black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa", size = 1698727, upload-time = "2025-09-19T00:31:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/66f26bfbbf84b949cc77a41a43e138d83b109502cd9c52dfc94070ca51f2/black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d", size = 1555679, upload-time = "2025-09-19T00:31:29.265Z" }, + { url = "https://files.pythonhosted.org/packages/bf/59/61475115906052f415f518a648a9ac679d7afbc8da1c16f8fdf68a8cebed/black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608", size = 1617453, upload-time = "2025-09-19T00:30:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5b/20fd5c884d14550c911e4fb1b0dae00d4abb60a4f3876b449c4d3a9141d5/black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f", size = 1333655, upload-time = "2025-09-19T00:30:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8e/319cfe6c82f7e2d5bfb4d3353c6cc85b523d677ff59edc61fdb9ee275234/black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0", size = 1742012, upload-time = "2025-09-19T00:33:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/94/cc/f562fe5d0a40cd2a4e6ae3f685e4c36e365b1f7e494af99c26ff7f28117f/black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4", size = 1581421, upload-time = "2025-09-19T00:35:25.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/67/6db6dff1ebc8965fd7661498aea0da5d7301074b85bba8606a28f47ede4d/black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e", size = 1655619, upload-time = "2025-09-19T00:30:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/10/10/3faef9aa2a730306cf469d76f7f155a8cc1f66e74781298df0ba31f8b4c8/black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a", size = 1342481, upload-time = "2025-09-19T00:31:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/0f724eb152bc9fc03029a9c903ddd77a288285042222a381050d27e64ac1/black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47", size = 1715243, upload-time = "2025-09-19T00:34:14.216Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/cb986ea2f0fabd0ee58668367724ba16c3a042842e9ebe009c139f8221c9/black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823", size = 1571246, upload-time = "2025-09-19T00:31:39.624Z" }, + { url = "https://files.pythonhosted.org/packages/82/ce/74cf4d66963fca33ab710e4c5817ceeff843c45649f61f41d88694c2e5db/black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140", size = 1631265, upload-time = "2025-09-19T00:31:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f3/9b11e001e84b4d1721f75e20b3c058854a748407e6fc1abe6da0aa22014f/black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933", size = 1326615, upload-time = "2025-09-19T00:31:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/63a45bfc36f73efe46731a3a71cb84e2112f7e0b049507025ce477f0f052/charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", size = 198805, upload-time = "2025-08-09T07:56:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/0c/52/8b0c6c3e53f7e546a5e49b9edb876f379725914e1130297f3b423c7b71c5/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", size = 142862, upload-time = "2025-08-09T07:56:57.751Z" }, + { url = "https://files.pythonhosted.org/packages/59/c0/a74f3bd167d311365e7973990243f32c35e7a94e45103125275b9e6c479f/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", size = 155104, upload-time = "2025-08-09T07:56:58.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/79/ae516e678d6e32df2e7e740a7be51dc80b700e2697cb70054a0f1ac2c955/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", size = 152598, upload-time = "2025-08-09T07:57:00.201Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/ef9c88464b126fa176f4ef4a317ad9b6f4d30b2cffbc43386062367c3e2c/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", size = 147391, upload-time = "2025-08-09T07:57:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/7a/03/cbb6fac9d3e57f7e07ce062712ee80d80a5ab46614684078461917426279/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", size = 145037, upload-time = "2025-08-09T07:57:02.638Z" }, + { url = "https://files.pythonhosted.org/packages/64/d1/f9d141c893ef5d4243bc75c130e95af8fd4bc355beff06e9b1e941daad6e/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", size = 156425, upload-time = "2025-08-09T07:57:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/c5/35/9c99739250742375167bc1b1319cd1cec2bf67438a70d84b2e1ec4c9daa3/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", size = 153734, upload-time = "2025-08-09T07:57:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/50/10/c117806094d2c956ba88958dab680574019abc0c02bcf57b32287afca544/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", size = 148551, upload-time = "2025-08-09T07:57:06.823Z" }, + { url = "https://files.pythonhosted.org/packages/61/c5/dc3ba772489c453621ffc27e8978a98fe7e41a93e787e5e5bde797f1dddb/charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", size = 98459, upload-time = "2025-08-09T07:57:08.031Z" }, + { url = "https://files.pythonhosted.org/packages/05/35/bb59b1cd012d7196fc81c2f5879113971efc226a63812c9cf7f89fe97c40/charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", size = 105887, upload-time = "2025-08-09T07:57:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, + { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, + { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "flake8" +version = "5.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version < '3.8.1'" }, + { name = "pycodestyle", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "pyflakes", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862, upload-time = "2022-08-03T23:21:27.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897, upload-time = "2022-08-03T23:21:25.027Z" }, +] + +[[package]] +name = "flake8" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.9'" }, + { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jiter" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/84/72/c28662416d9807bb5a38625eadedb82d4bd14fd2700c308ece7acdb8e89f/jiter-0.9.1.tar.gz", hash = "sha256:7852990068b6e06102ecdc44c1619855a2af63347bfb5e7e009928dcacf04fdd", size = 162540, upload-time = "2025-05-18T17:47:14.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/5f/7f6aaca7943c644b4fd220650771f39dbfb74f9690efc6fb8c0d4092a399/jiter-0.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c0163baa7ee85860fdc14cc39263014500df901eeffdf94c1eab9a2d713b2a9d", size = 312882, upload-time = "2025-05-18T17:45:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/aac9eafc5d46bdf5c4f127ac1ce85e434d003bb5e3ae886f5e726a988cf6/jiter-0.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:514d4dd845e0af4da15112502e6fcb952f0721f27f17e530454e379472b90c14", size = 311743, upload-time = "2025-05-18T17:45:16.196Z" }, + { url = "https://files.pythonhosted.org/packages/b8/54/fab1f4d8634af7bb1ad6dc49bee50ea9f649de0e5309c80192ace739f968/jiter-0.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b879faee1cc1a67fde3f3f370041239fd260ac452bd53e861aa4a94a51e3fd02", size = 1085889, upload-time = "2025-05-18T17:45:17.883Z" }, + { url = "https://files.pythonhosted.org/packages/bd/86/bf4ed251d8035d5d72a46c8f9969bd5054fad052371cbea0cb161060e660/jiter-0.9.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20a5ce641f93bfb8d8e336f8c4a045e491652f41eaacc707b15b245ece611e72", size = 1117896, upload-time = "2025-05-18T17:45:19.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/40/b04c40deccd5edd5f2a3853f4a80dc0ddbe157d1d523a573fb3d224315fc/jiter-0.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8575b1d2b49df04ca82d658882f4a432b7ed315a69126a379df4d10aeb416021", size = 1211956, upload-time = "2025-05-18T17:45:21.606Z" }, + { url = "https://files.pythonhosted.org/packages/85/f0/114e9893e4ef5b423718efe9b3da01117539c333f06ef19543c68c8b7ed1/jiter-0.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc61831699904e0c58e82943f529713833db87acd13f95a3c0feb791f862d47b", size = 1219691, upload-time = "2025-05-18T17:45:23.061Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/1aeac4541ce1c59c65dc76dbab642232da3d8db0581df3e61b8943033bd7/jiter-0.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb733faf4d0e730d6663873249c1acb572fc8bd9dae3836ceda69751f27c5be", size = 352604, upload-time = "2025-05-18T17:45:24.485Z" }, + { url = "https://files.pythonhosted.org/packages/6b/27/446ec6ca0a25d9d2f45ad546633a2b4a1b6a7f28fb6819c7056b163c5aee/jiter-0.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d903b3bb917c0df24f2ef62f587c8f32f6003cb2f97264109ca56c023262557f", size = 1147136, upload-time = "2025-05-18T17:45:25.832Z" }, + { url = "https://files.pythonhosted.org/packages/09/9d/c8540bc097b07e106d060c21395c6fa6561223e7366c948a04ef0aa39979/jiter-0.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:eac3eb5206845b170142c016ae467eca523a25459dc9c53fcd8e154ea263406c", size = 1255843, upload-time = "2025-05-18T17:45:27.513Z" }, + { url = "https://files.pythonhosted.org/packages/d3/61/9b377ecf4e09e325e90f77a7a4859ec933162f58ff5c6b7730aff6352033/jiter-0.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7ea0c20cfc61acc5335bb8ee36d639e6a4ded03f34f878b2b3038bb9f3bb553c", size = 1257536, upload-time = "2025-05-18T17:45:29.304Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f6/b6754e11ac9d02f05a2d713c0846ce813a69c1f6f7de7f1ae216c4e35ace/jiter-0.9.1-cp310-cp310-win32.whl", hash = "sha256:0f8f812dd6d2b4112db9ab4c1079c4fe73e553a500e936657fdda394fa2517e1", size = 214064, upload-time = "2025-05-18T17:45:31.037Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cb/7b9c5d6f73499d1fb5e97e36e8078f3bea00d7541a973117eccf9db1e079/jiter-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:f7f0198889170e7af6210509803e6527b402efc6c26f42e2896883597a10426f", size = 209952, upload-time = "2025-05-18T17:45:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/9f9deaef471e346354c832b6627e0d1b9ba3d9611d0e0fd394c2acf2a615/jiter-0.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b8564e3198c4c8d835fc95cc54d6bcbd2fd8dc33a047fecc12c208491196995", size = 312737, upload-time = "2025-05-18T17:45:34.456Z" }, + { url = "https://files.pythonhosted.org/packages/36/00/76fa6d519f8289aad32ec1caf3716eb700ba48e3212d1dda71e74c385a5c/jiter-0.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90b92044588d14efe89b394eca735adc4ac096eba82dc75d93c3083b1eebce8d", size = 313357, upload-time = "2025-05-18T17:45:36.672Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e9/f864ebe9ddf07761d5bdd3148b45a5d433c6cbce7c7e8be29baf806fa612/jiter-0.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3505f7f419b355c7788fcaae0dfc4c6ccbc50c0dc3633a2da797e841c5a423dc", size = 1085946, upload-time = "2025-05-18T17:45:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/82/a1/ed02d4c86d620989dcd392366daa67198961eedaf2e66f7a68f0d3846dba/jiter-0.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93af8c3f4a3bf145c690e857a945eb5c655534bf95c67e1447d85c02e5af64d7", size = 1118090, upload-time = "2025-05-18T17:45:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/01/d107531d215a57cda3cbc4adfcf3119166dd32adc1c332c1f3f36efd3484/jiter-0.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43b81dd21e260a249780764921b1f9a6379cb31e24e7b61e6bf0799f38ec4b91", size = 1212231, upload-time = "2025-05-18T17:45:40.738Z" }, + { url = "https://files.pythonhosted.org/packages/45/1e/6801a81a2ef1f917fe9a7d2139e576dd4f53497c309dab9461136922709c/jiter-0.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db639fad5631b3d1692609f6dd77b64e8578321b7aeec07a026acd2c867c04a5", size = 1219263, upload-time = "2025-05-18T17:45:42.698Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d4/40082e8666cfdb24461855e9bb29fe77f063cc65a6c903291f2e5225f780/jiter-0.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15356b943e70ca7ab3b587ffaffadc0158467f6c4e0b491e52a0743c4bdf5ba1", size = 350364, upload-time = "2025-05-18T17:45:44.257Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/09bc72dd143f76acd55e04c3a45b9f9ee3ed28e00b49924e3702ad041812/jiter-0.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53a7033a46141ff815518a6972d657c75d8f5946b9315e1c25b07e9677c1ff6c", size = 1146802, upload-time = "2025-05-18T17:45:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/5b/34/9d15a9c04d5760537b432134447bde94b936ec73dc922b4d14a48def2e1f/jiter-0.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:68cf519a6f00b8127f9be64a37e97e978094438abced5adebe088a98c64bdcff", size = 1256019, upload-time = "2025-05-18T17:45:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/01/1fcd165fb28968a54bb46a209d5919f7649b96608eef7dc4622ea378b95a/jiter-0.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9098abdd34cd9ddeb04768cc4f5fc725ebd9a52978c488da74e58a837ce93506", size = 1257610, upload-time = "2025-05-18T17:45:48.902Z" }, + { url = "https://files.pythonhosted.org/packages/9f/87/93ac6a57331dd90e4c896ac852bf8ce6b28b40dace4b9698a207dbb99af2/jiter-0.9.1-cp311-cp311-win32.whl", hash = "sha256:7179ce96aecd096af890dd57b84133e47a59fbde32a77734f09bafa6a4da619e", size = 214515, upload-time = "2025-05-18T17:45:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ee/3678b8a3bd5f6471d0a492540e7ff9c63db278d844214458ec5cfb22adb2/jiter-0.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:e6517f5b7b6f60fd77fc1099572f445be19553c6f61b907ab5b413fb7179663f", size = 212258, upload-time = "2025-05-18T17:45:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a7/5b3ce91b5bb83bf47e85ab2efda26a1706fb52498a2abe79df09af7dfa8f/jiter-0.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f330c5023ce4153ceb3e8abe76ecab8c5b525824bcec4e781791d044e5b5fc3a", size = 307494, upload-time = "2025-05-18T17:45:53.639Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/006ebbb5ab55fd9f47c219f9de7fdedd38694c158ddd6760a15f7a6fcdc8/jiter-0.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:77de4d2d529ece2d43fc0dbe90971e9e18f42ed6dd50b40fe232e799efb72c29", size = 312782, upload-time = "2025-05-18T17:45:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/17/da/a437705850c8cf6b8c93769ff6fcb3abcbfeb9c12b690c5f1631682d4286/jiter-0.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed3eec217a70762a01ecfbecea27eda91d7d5792bdef41096d2c672a9e3c1fe", size = 1087076, upload-time = "2025-05-18T17:45:56.866Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/f463a03de974d437abc312a0ca6212e2b014b7023a880fd6956ebfde15c7/jiter-0.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d000bb8b9b3a90fb61ff864869461c56ad2dad5f0fa71127464cb65e69ec864b", size = 1118826, upload-time = "2025-05-18T17:45:58.359Z" }, + { url = "https://files.pythonhosted.org/packages/6a/04/4d9289d8610f2b10886b4bd32b0c6e036fdeabc86cc9a902e50434a066bd/jiter-0.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3610aed85fad26d5e107ce4e246c236b612e539b382d490761aacc4aa5d7cdbf", size = 1213155, upload-time = "2025-05-18T17:45:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/f3/4c/851c0a7c95e333d5213558fc76d217a7760de8b704299c007537af49e1de/jiter-0.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae8f1f42f4b0ed244f88bb863d0777292e76e43ee2dc0dac4d63fe29bee183e5", size = 1215024, upload-time = "2025-05-18T17:46:01.083Z" }, + { url = "https://files.pythonhosted.org/packages/8f/24/9c62f5775645715ded77a4cf03b9f3c36d4909ee35b07f65bb4ccaad4bfd/jiter-0.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2082da43e7b6174c3522a6905a9ee9187c9771e32cad7ab58360f189595a7c3f", size = 350280, upload-time = "2025-05-18T17:46:02.912Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/54a4b1074f1f048ca822a2f4a738fa7b623203540a59ec99d0b0277c38ef/jiter-0.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d82b2b8bc089c4ebff99907bdb890730e05c58169d5493473c916518f8d29f5c", size = 1150978, upload-time = "2025-05-18T17:46:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/caaa8d274ba82486dfb582e32f431412f2e178344ebf6a231b8606c048fd/jiter-0.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8b7214d4064759ff34846311cabcf49715e8a7286a4431bc7444537ee2f21b1a", size = 1257583, upload-time = "2025-05-18T17:46:06.113Z" }, + { url = "https://files.pythonhosted.org/packages/19/f7/a5f991075b16b76b15e4da7939243f373ff4369ce41145be428c7c43d905/jiter-0.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:136a635797b27aeb5cacea4d0ffeff5c80081089217c5891bd28968e5df97824", size = 1258268, upload-time = "2025-05-18T17:46:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/94/8f/6fabe1aa77637be629e73db2ee3059889b893c4be391f0e038b71948d208/jiter-0.9.1-cp312-cp312-win32.whl", hash = "sha256:5da9a4e2939c4af7617fe01f7e3978fba224d93def72bc748d173f148a8b637f", size = 214250, upload-time = "2025-05-18T17:46:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/6f118d22acf5930d5a46c4f6853eead883af8c097d83e2a2971308864423/jiter-0.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:d1434a05965d0c1f033061f21553fef5c3a352f3e880a0f503e79e6b639db10c", size = 211070, upload-time = "2025-05-18T17:46:11.39Z" }, + { url = "https://files.pythonhosted.org/packages/e2/36/4b5c7c96ce4795376e546bcabd96d8fe8667c9fdeb946523ca382cc30eaa/jiter-0.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cb0629af6a12804ace5f093884c2f14d5075d95951a086054e106cfdb6b8862f", size = 307047, upload-time = "2025-05-18T17:46:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/3e/20/7635fb02fe62cd90899dc1c64c972c1470106eede55ce35fc6e3868251af/jiter-0.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d15cc2b5602fb5a16689afb507b27c650167152203394efa429a5139553dd993", size = 311796, upload-time = "2025-05-18T17:46:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/e4/43/7e4a38c63b9f1a5795d406a7cf1e8a42af0e51d05d5c5b866708a345d49e/jiter-0.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffbf9279273b41fb8c4360ad2590a8eea82b36665728f57b0d7b095a904016d9", size = 1086812, upload-time = "2025-05-18T17:46:15.765Z" }, + { url = "https://files.pythonhosted.org/packages/30/17/3d5ad7a1e12bb172040c2e206068ee766a320c6b6327a0a52a9c05bf4cd6/jiter-0.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fca2935783d4309eed77ed2acd625f93a07b79693f7d8e58e3c18ac8981e9ea", size = 1118218, upload-time = "2025-05-18T17:46:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f7/9f46d976a91f339898783962043c36b8c9fe103135f264ae25dddad9838e/jiter-0.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3f5f14d63924d3b226236c746ceb37f5ac9d3ce1251762819024f84904b4a0f", size = 1211346, upload-time = "2025-05-18T17:46:19.823Z" }, + { url = "https://files.pythonhosted.org/packages/93/71/cf594ec8c76188b5e42fc4f00a9cdfb3f675631234f5a1ac5413fe6684cb/jiter-0.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d43dcddb437096ac48e85f6be8355d806ab9246051f95263933fa5e18d026aa", size = 1214466, upload-time = "2025-05-18T17:46:21.639Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e5/efd89f27838ea9d8257c9bc8edd58a953e06ca304c7d2b397fdd2a932e51/jiter-0.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19773c6f730523effbca88c4a15658b481cf81e4c981fcd1212dd4beaa0cd37a", size = 350245, upload-time = "2025-05-18T17:46:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/b7960c8a04d593687659007e6b7f911ef3f877eb11cd2503267ad5b2da0b/jiter-0.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:01fcc08b6d3e29562d72edfcd6c5b0aab30b964fb0c99ad8287c2dffeb6fd38c", size = 1149223, upload-time = "2025-05-18T17:46:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/4777b5a70febeece230593a82a69d0d19b5b6e36a8b3afcc4b43528c2657/jiter-0.9.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:448afc1a801a518ed438667229f380bb0b8503f379d170ac947575cb7e1e4edf", size = 1257025, upload-time = "2025-05-18T17:46:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c1/8fe3483537d85bc381bdab2a4952707d92944b1ac32074f7b33de188c2d0/jiter-0.9.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f321fb984ed7544e77346714a25ffa5bbefddd1adcc32c8fba49030a119a31c6", size = 1257882, upload-time = "2025-05-18T17:46:29.21Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1a/4453114fb7b3722f8d232b3c08114535e455d7d2d4d83b44cede53ed42ae/jiter-0.9.1-cp313-cp313-win32.whl", hash = "sha256:7db7c9a95d72668545606aeaf110549f4f42679eaa3ce5c32f8f26c1838550d8", size = 214946, upload-time = "2025-05-18T17:46:30.607Z" }, + { url = "https://files.pythonhosted.org/packages/15/d0/237d7dbaaafb08a6f719c8495663b76d70d6c5880a02c7b092f21292458b/jiter-0.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a6b750ef1201fe4c431f869705607ece4adaf592e497efb6bc4138efaebb4f59", size = 209888, upload-time = "2025-05-18T17:46:31.89Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/e90c89adbea8342b6e470f3be9c213b628ae3842810553df15d5afb386ce/jiter-0.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4096dba935aa2730c7642146b065855a0f5853fd9bbe22de9e3dd39fcacc37fe", size = 311645, upload-time = "2025-05-18T17:46:33.196Z" }, + { url = "https://files.pythonhosted.org/packages/29/40/98fee5bab390c27d20ba82c73d12afd1db89aabeef641ae7629a31a7100f/jiter-0.9.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13ad975e0d9d2f7e54b30d9ae8e2e1c97be422e75606bddc67427721ad13cd1c", size = 352754, upload-time = "2025-05-18T17:46:34.457Z" }, + { url = "https://files.pythonhosted.org/packages/9b/17/b0fa4ee5bdcb252b2407fc9528f11d8af717b7218455d23018cf314ccf6a/jiter-0.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f11992b20f8a2d336b98b31bff4d8bfcc4bd5aef7840594e32d6cb44fb9b96cf", size = 212573, upload-time = "2025-05-18T17:46:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1c7438d66969a13938266492de65daf752754ec59f2a3f3716027c7d708f/jiter-0.9.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:95065923a49ae387bab62b1bf5f798beb12e6fb4469a079fdd0ecad64b40b272", size = 313516, upload-time = "2025-05-18T17:46:37.568Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d9/3a6300309e312f8ed529ae57d565f69abdb520e4f12460cefa7996d0716c/jiter-0.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a179fbc5c7922844a673be35099a3036a7276dc63753c6c81a77c3cb525f2f8d", size = 308161, upload-time = "2025-05-18T17:46:39.697Z" }, + { url = "https://files.pythonhosted.org/packages/b3/91/2aca15be38514daf8f1a1460fd9c4b652ed09148fe109520298858be7928/jiter-0.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd30dc5c0183d31faf30ce8279d723809c54b3fe6d95d922d4a4b31bc462799", size = 1086100, upload-time = "2025-05-18T17:46:41.176Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6f/f7ba3dfe7be08bf58939324e0bb4f4aa605eff7f2c2ac140a41221cf50a4/jiter-0.9.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9765512bdeae269843e6615377f48123432da247e18048d05e9c5685377c241c", size = 1118922, upload-time = "2025-05-18T17:46:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/4e/b1f4d9bdba81de293e1b8672598300a9195cf3d77b0acc5f331a75695b58/jiter-0.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f15cdbdc1e1e89e0d9ea581de63e03975043a4b40ab87d5554fdc440357b771", size = 1212327, upload-time = "2025-05-18T17:46:44.193Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ab/e417aaf5a62067bd91c5f7ed4e5ab83bd46f349449adde1159ad8e2d3a21/jiter-0.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1a639b2cfe56b5b687c678ed45d68f46dfb922c2f338fdfb227eb500053929d", size = 1220860, upload-time = "2025-05-18T17:46:45.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/50/c5ba756c641ca8ebc1e4ff07c03ce5c8ef5052b0238f514436f8de3c9fc4/jiter-0.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41955c9d83c8470de9cc64c97b04a3ffd2f32815bb2c4307f44d8e21542b74df", size = 344077, upload-time = "2025-05-18T17:46:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b3/bd7d8d4bad65aa1f4a20562233080054149785c0d7f7b9027e761335d882/jiter-0.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f26f6d42c330e26a6ba3471b390364faad96f3ca965a6c579957810b0c078efa", size = 1148785, upload-time = "2025-05-18T17:46:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/c0/12/bfd9a167709f96171312d1e0ae2c1be70a167abcc3bff6f3441967e3626a/jiter-0.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a23e01bd7e918f27f02d3df8721b8a395211070a8a65aeb353209b8c72720cf", size = 1255962, upload-time = "2025-05-18T17:46:50.775Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/3a79020862d2511b854b350bc9229cf228fd38b836e94f274ca940e22e95/jiter-0.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8a96ad217989dd9df661711c3fa2e6fb2601c4bbb482e89718110bdafbc16c9e", size = 1257561, upload-time = "2025-05-18T17:46:52.291Z" }, + { url = "https://files.pythonhosted.org/packages/93/d3/7f6f8e57613d4947a872980befa6af19de9252e310ea4a512eed0fe1e064/jiter-0.9.1-cp38-cp38-win32.whl", hash = "sha256:4b180e7baa4747b3834c5a9202b1ba30dc64797f45236d9142cdb2a8807763cf", size = 215019, upload-time = "2025-05-18T17:46:54.068Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/b6f0cd60c8f702936f253644a92dee19e2c82010290e4607af462033351f/jiter-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:baf881de1fbc7b3343cce24f75a2ab6350e03fc13d16d00f452929788a6cdc3f", size = 199563, upload-time = "2025-05-18T17:46:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3a/a8a4768af26578c87894bb130bcd6fb6c97f4cb36ed7a20a664412d41935/jiter-0.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ec95aa1b433c50b2b129456b4680b239ec93206ea3f86cfd41b6a70be5beb2f3", size = 313942, upload-time = "2025-05-18T17:46:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/63/74/05977891db48000d985a5f573493c43adf0f190eada670e51b92c9ed9139/jiter-0.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d92cb50d135dbdd33b638fa2e0c6af25e1d635d38da13aa9ab05d021fb0c869", size = 308160, upload-time = "2025-05-18T17:46:58.439Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/75f529e90442c8ad41acd8cf08323a4f3dcaa105710b2c8a1fda56e3a462/jiter-0.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b146dc2464f1d96007271d08bdf79288a5f1aa4aae5329eb79dcffb1181c703e", size = 1086503, upload-time = "2025-05-18T17:47:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fa/02532a7ce7b712c576125d4f2614e77bc897c95b2b15e21ee25f42b3ff34/jiter-0.9.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcf20ba858658ecd54b4710172d92009afa66d41d967c86d11607592a3c220fa", size = 1120444, upload-time = "2025-05-18T17:47:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/91/c2/ab8cebaea6f2691eddcc5b6c67deb1399adbd85f12ad836f7cd77be78bf8/jiter-0.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:147fccc44bebdb672d4c601e9312730488b840d415e201e89c8ea0929a63dacf", size = 1212370, upload-time = "2025-05-18T17:47:03.145Z" }, + { url = "https://files.pythonhosted.org/packages/13/e3/90dddb7877b67cc0e1ddb864c2ca74314def26ff6542431a6e3061e0f805/jiter-0.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a428061aae26efaa6fb690ef9e7d6224aefe4eef7524165d073beb3cdad75f6f", size = 1221210, upload-time = "2025-05-18T17:47:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/90ee847519a94a4a1a8bad7addce7019f424aea03c55eacf068469226760/jiter-0.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7164d92bb901784bd3c098ac0b0beae4306ea6c741dbd3a375449a8affc5366", size = 353774, upload-time = "2025-05-18T17:47:06.445Z" }, + { url = "https://files.pythonhosted.org/packages/59/a6/614a5d672d4b9c6bc9ad34579f0522577a0a78cc265069fca96543a832ca/jiter-0.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93049a562233808914a2b938b0c745d7049db1667b3f42f0f5cf48e617393ba5", size = 1148581, upload-time = "2025-05-18T17:47:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/c100147c310361fa83e25c4c6ce17723532147580252962b89e6085795c2/jiter-0.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f6dcf2cb16cc15d82a018e20eeaf169e6f6cd8c426f4c312ebe11710c623bed2", size = 1256636, upload-time = "2025-05-18T17:47:09.189Z" }, + { url = "https://files.pythonhosted.org/packages/51/9a/dc82e218ba839052899df555e34f16b8ad1d7da9c01be208f65a5bf0083c/jiter-0.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2da9d485a7c526817cde9ff8b3394fa50ff5b782b86b6896378a3ba8844550f2", size = 1258099, upload-time = "2025-05-18T17:47:10.568Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/d853e069624038950265ac0e877985b249049b624e925dab6cd11035140c/jiter-0.9.1-cp39-cp39-win32.whl", hash = "sha256:ea58c155d827d24e5ba8d7958ec4738b26be0894c0881a91d88b39ff48bb06c9", size = 214611, upload-time = "2025-05-18T17:47:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/7b6b1ee6e3d9d1a06237bbdfe4c6bb21baf323d3f70a0cc8f203de40c6b2/jiter-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:be2e911ecdb438951290c2079fe4190e7cc5be9e849df4caeb085b83ed620ff6", size = 211171, upload-time = "2025-05-18T17:47:13.47Z" }, +] + +[[package]] +name = "jiter" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/21/7dd1235a19e26979be6098e87e4cced2e061752f3a40a17bbce6dea7fae1/jiter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3893ce831e1c0094a83eeaf56c635a167d6fa8cc14393cc14298fd6fdc2a2449", size = 309875, upload-time = "2025-09-15T09:18:48.41Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/462b54708aa85b135733ccba70529dd68a18511bf367a87c5fd28676c841/jiter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25c625b9b61b5a8725267fdf867ef2e51b429687f6a4eef211f4612e95607179", size = 316505, upload-time = "2025-09-15T09:18:51.057Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/14e2eeaac6a47bff27d213834795472355fd39769272eb53cb7aa83d5aa8/jiter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4ca85fb6a62cf72e1c7f5e34ddef1b660ce4ed0886ec94a1ef9777d35eaa1f", size = 337613, upload-time = "2025-09-15T09:18:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ed/a5f1f8419c92b150a7c7fb5ccba1fb1e192887ad713d780e70874f0ce996/jiter-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:572208127034725e79c28437b82414028c3562335f2b4f451d98136d0fc5f9cd", size = 361438, upload-time = "2025-09-15T09:18:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/70682c023dfcdd463a53faf5d30205a7d99c51d70d3e303c932d0936e5a2/jiter-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494ba627c7f550ad3dabb21862864b8f2216098dc18ff62f37b37796f2f7c325", size = 486180, upload-time = "2025-09-15T09:18:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/7c/39/020d08cbab4eab48142ad88b837c41eb08a15c0767fdb7c0d3265128a44b/jiter-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8da18a99f58bca3ecc2d2bba99cac000a924e115b6c4f0a2b98f752b6fbf39a", size = 376681, upload-time = "2025-09-15T09:18:57.553Z" }, + { url = "https://files.pythonhosted.org/packages/52/10/b86733f6e594cf51dd142f37c602d8df87c554c5844958deaab0de30eb5d/jiter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ffd3b0fff3fabbb02cc09910c08144db6bb5697a98d227a074401e01ee63dd", size = 348685, upload-time = "2025-09-15T09:18:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ee/8861665e83a9e703aa5f65fddddb6225428e163e6b0baa95a7f9a8fb9aae/jiter-0.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fe6530aa738a4f7d4e4702aa8f9581425d04036a5f9e25af65ebe1f708f23be", size = 385573, upload-time = "2025-09-15T09:19:00.593Z" }, + { url = "https://files.pythonhosted.org/packages/25/74/05afec03600951f128293813b5a208c9ba1bf587c57a344c05a42a69e1b1/jiter-0.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e35d66681c133a03d7e974e7eedae89720fe8ca3bd09f01a4909b86a8adf31f5", size = 516669, upload-time = "2025-09-15T09:19:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/93/d1/2e5bfe147cfbc2a5eef7f73eb75dc5c6669da4fa10fc7937181d93af9495/jiter-0.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59459beca2fbc9718b6f1acb7bfb59ebc3eb4294fa4d40e9cb679dafdcc6c60", size = 508767, upload-time = "2025-09-15T09:19:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/87/50/597f71307e10426b5c082fd05d38c615ddbdd08c3348d8502963307f0652/jiter-0.11.0-cp310-cp310-win32.whl", hash = "sha256:b7b0178417b0dcfc5f259edbc6db2b1f5896093ed9035ee7bab0f2be8854726d", size = 205476, upload-time = "2025-09-15T09:19:05.594Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/1e5214b3272e311754da26e63edec93a183811d4fc2e0118addec365df8b/jiter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:11df2bf99fb4754abddd7f5d940a48e51f9d11624d6313ca4314145fcad347f0", size = 204708, upload-time = "2025-09-15T09:19:06.955Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/a69fefeef09c2eaabae44b935a1aa81517e49639c0a0c25d861cb18cd7ac/jiter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cb5d9db02979c3f49071fce51a48f4b4e4cf574175fb2b11c7a535fa4867b222", size = 309503, upload-time = "2025-09-15T09:19:08.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d5/a6aba9e6551f32f9c127184f398208e4eddb96c59ac065c8a92056089d28/jiter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1dc6a123f3471c4730db7ca8ba75f1bb3dcb6faeb8d46dd781083e7dee88b32d", size = 317688, upload-time = "2025-09-15T09:19:09.918Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/5e86f57c1883971cdc8535d0429c2787bf734840a231da30a3be12850562/jiter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09858f8d230f031c7b8e557429102bf050eea29c77ad9c34c8fe253c5329acb7", size = 337418, upload-time = "2025-09-15T09:19:11.078Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/a71d8a24c2a70664970574a8e0b766663f5ef788f7fe1cc20ee0c016d488/jiter-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbe2196c4a0ce760925a74ab4456bf644748ab0979762139626ad138f6dac72d", size = 361423, upload-time = "2025-09-15T09:19:13.286Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e5/b09076f4e7fd9471b91e16f9f3dc7330b161b738f3b39b2c37054a36e26a/jiter-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5beb56d22b63647bafd0b74979216fdee80c580c0c63410be8c11053860ffd09", size = 486367, upload-time = "2025-09-15T09:19:14.546Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/98cb3a36f5e62f80cd860f0179f948d9eab5a316d55d3e1bab98d9767af5/jiter-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97025d09ef549795d8dc720a824312cee3253c890ac73c621721ddfc75066789", size = 376335, upload-time = "2025-09-15T09:19:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d8/ec74886497ea393c29dbd7651ddecc1899e86404a6b1f84a3ddab0ab59fd/jiter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50880a6da65d8c23a2cf53c412847d9757e74cc9a3b95c5704a1d1a24667347", size = 348981, upload-time = "2025-09-15T09:19:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/24/93/d22ad7fa3b86ade66c86153ceea73094fc2af8b20c59cb7fceab9fea4704/jiter-0.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:452d80a1c86c095a242007bd9fc5d21b8a8442307193378f891cb8727e469648", size = 385797, upload-time = "2025-09-15T09:19:19.121Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bd/e25ff4a4df226e9b885f7cb01ee4b9dc74e3000e612d6f723860d71a1f34/jiter-0.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e84e58198d4894668eec2da660ffff60e0f3e60afa790ecc50cb12b0e02ca1d4", size = 516597, upload-time = "2025-09-15T09:19:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/be/fb/beda613db7d93ffa2fdd2683f90f2f5dce8daf4bc2d0d2829e7de35308c6/jiter-0.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df64edcfc5dd5279a791eea52aa113d432c933119a025b0b5739f90d2e4e75f1", size = 508853, upload-time = "2025-09-15T09:19:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/c5b0d93490634e41e38e2a15de5d54fdbd2c9f64a19abb0f95305b63373c/jiter-0.11.0-cp311-cp311-win32.whl", hash = "sha256:144fc21337d21b1d048f7f44bf70881e1586401d405ed3a98c95a114a9994982", size = 205140, upload-time = "2025-09-15T09:19:23.351Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/c347c0e6f5796e97d4356b7e5ff0ce336498b7f4ef848fae621a56f1ccf3/jiter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b0f32e644d241293b892b1a6dd8f0b9cc029bfd94c97376b2681c36548aabab7", size = 204311, upload-time = "2025-09-15T09:19:24.591Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510, upload-time = "2025-09-15T09:19:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521, upload-time = "2025-09-15T09:19:27.525Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214, upload-time = "2025-09-15T09:19:28.727Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280, upload-time = "2025-09-15T09:19:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895, upload-time = "2025-09-15T09:19:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421, upload-time = "2025-09-15T09:19:32.746Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932, upload-time = "2025-09-15T09:19:34.612Z" }, + { url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959, upload-time = "2025-09-15T09:19:35.994Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187, upload-time = "2025-09-15T09:19:37.426Z" }, + { url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461, upload-time = "2025-09-15T09:19:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664, upload-time = "2025-09-15T09:19:40.096Z" }, + { url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520, upload-time = "2025-09-15T09:19:41.798Z" }, + { url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021, upload-time = "2025-09-15T09:19:43.523Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384, upload-time = "2025-09-15T09:19:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389, upload-time = "2025-09-15T09:19:46.094Z" }, + { url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519, upload-time = "2025-09-15T09:19:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198, upload-time = "2025-09-15T09:19:49.116Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835, upload-time = "2025-09-15T09:19:50.468Z" }, + { url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655, upload-time = "2025-09-15T09:19:51.726Z" }, + { url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135, upload-time = "2025-09-15T09:19:53.075Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063, upload-time = "2025-09-15T09:19:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139, upload-time = "2025-09-15T09:19:55.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369, upload-time = "2025-09-15T09:19:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538, upload-time = "2025-09-15T09:19:58.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737, upload-time = "2025-09-15T09:19:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183, upload-time = "2025-09-15T09:20:01.442Z" }, + { url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225, upload-time = "2025-09-15T09:20:03.102Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414, upload-time = "2025-09-15T09:20:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223, upload-time = "2025-09-15T09:20:05.631Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306, upload-time = "2025-09-15T09:20:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565, upload-time = "2025-09-15T09:20:08.283Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465, upload-time = "2025-09-15T09:20:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581, upload-time = "2025-09-15T09:20:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102, upload-time = "2025-09-15T09:20:12.175Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477, upload-time = "2025-09-15T09:20:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004, upload-time = "2025-09-15T09:20:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855, upload-time = "2025-09-15T09:20:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802, upload-time = "2025-09-15T09:20:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405, upload-time = "2025-09-15T09:20:18.918Z" }, + { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102, upload-time = "2025-09-15T09:20:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d9/51cf35d92bea21f2051da8ca2328831589e67e2bf971e85b1a6e6c0d2030/jiter-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:719891c2fb7628a41adff4f2f54c19380a27e6fdfdb743c24680ef1a54c67bd0", size = 312764, upload-time = "2025-09-15T09:20:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/da/48/eae309ce5c180faa1bb45e378a503717da22ceb2b0488f78e548f97c2b6b/jiter-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df7f1927cbdf34cb91262a5418ca06920fd42f1cf733936d863aeb29b45a14ef", size = 305861, upload-time = "2025-09-15T09:20:22.603Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/13b80e18b0331f0fecc09cb2f09f722530b9a395006941b01491fe58baea/jiter-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e71ae6d969d0c9bab336c5e9e2fabad31e74d823f19e3604eaf96d9a97f463df", size = 339507, upload-time = "2025-09-15T09:20:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/c2fd1512873d3f23d24537e97765e7090a00de466516aa442b994b064918/jiter-0.11.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5661469a7b2be25ade3a4bb6c21ffd1e142e13351a0759f264dfdd3ad99af1ab", size = 363751, upload-time = "2025-09-15T09:20:25.12Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/48d156c742e75d33b9c8be44b1142d233823be491acdb1009629e4109e6a/jiter-0.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76c15ef0d3d02f8b389066fa4c410a0b89e9cc6468a1f0674c5925d2f3c3e890", size = 488591, upload-time = "2025-09-15T09:20:26.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/fd/214452149f63847b791b1f6e9558f59e94674c47418c03e9787236ac8656/jiter-0.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63782a1350917a27817030716566ed3d5b3c731500fd42d483cbd7094e2c5b25", size = 378906, upload-time = "2025-09-15T09:20:27.637Z" }, + { url = "https://files.pythonhosted.org/packages/de/91/25e38fbbfc17111d7b70b24290a41d611cc2a27fa6cd0ed84ddae38ec3e6/jiter-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a7092b699646a1ddc03a7b112622d9c066172627c7382659befb0d2996f1659", size = 350288, upload-time = "2025-09-15T09:20:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/d6d2eefa9f0ff6ac6b725f5164a94f15bb4dee68584b5054043d735803da/jiter-0.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f637b8e818f6d75540f350a6011ce21252573c0998ea1b4365ee54b7672c23c5", size = 388442, upload-time = "2025-09-15T09:20:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e4/cd7e27852de498d441a575a147ac7a15cf66768ae2cde8c43ea5b464c827/jiter-0.11.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a624d87719e1b5d09c15286eaee7e1532a40c692a096ea7ca791121365f548c1", size = 518273, upload-time = "2025-09-15T09:20:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/6681a9a503141752b33c92c58512ed8da13849ed3dbf88a3f8aba9bfb255/jiter-0.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9d0146d8d9b3995821bb586fc8256636258947c2f39da5bab709f3a28fb1a0b", size = 510254, upload-time = "2025-09-15T09:20:32.889Z" }, + { url = "https://files.pythonhosted.org/packages/38/32/df1a06f397074da35cf8fe79ec07da203358a2912b2a6349a0d4a88a1e0a/jiter-0.11.0-cp39-cp39-win32.whl", hash = "sha256:d067655a7cf0831eb8ec3e39cbd752995e9b69a2206df3535b3a067fac23b032", size = 207076, upload-time = "2025-09-15T09:20:34.196Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/11bc61fa76fd6197f5baa8576614852ee8586a16c2f25085edc6b47a369d/jiter-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:f05d03775a11aaf132c447436983169958439f1219069abf24662a672851f94e", size = 206077, upload-time = "2025-09-15T09:20:35.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/ce100253c80063a7b8b406e1d1562657fd4b9b4e1b562db40e68645342fb/jiter-0.11.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:902b43386c04739229076bd1c4c69de5d115553d982ab442a8ae82947c72ede7", size = 336380, upload-time = "2025-09-15T09:20:36.867Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" }, + { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "openai" +version = "1.108.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter", version = "0.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jiter", version = "0.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pydantic", version = "2.11.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/7a/3f2fbdf82a22d48405c1872f7c3176a705eee80ff2d2715d29472089171f/openai-1.108.1.tar.gz", hash = "sha256:6648468c1aec4eacfa554001e933a9fa075f57bacfc27588c2e34456cee9fef9", size = 563735, upload-time = "2025-09-19T16:52:20.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/87/6ad18ce0e7b910e3706480451df48ff9e0af3b55e5db565adafd68a0706a/openai-1.108.1-py3-none-any.whl", hash = "sha256:952fc027e300b2ac23be92b064eac136a2bc58274cec16f5d2906c361340d59b", size = 948394, upload-time = "2025-09-19T16:52:18.369Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "annotated-types", marker = "python_full_version < '3.9'" }, + { name = "pydantic-core", version = "2.27.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.9'" }, + { name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938, upload-time = "2024-12-18T11:27:14.406Z" }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684, upload-time = "2024-12-18T11:27:16.489Z" }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169, upload-time = "2024-12-18T11:27:22.16Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227, upload-time = "2024-12-18T11:27:25.097Z" }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695, upload-time = "2024-12-18T11:27:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662, upload-time = "2024-12-18T11:27:30.798Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370, upload-time = "2024-12-18T11:27:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813, upload-time = "2024-12-18T11:27:37.111Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287, upload-time = "2024-12-18T11:27:40.566Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414, upload-time = "2024-12-18T11:27:43.757Z" }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301, upload-time = "2024-12-18T11:27:47.36Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685, upload-time = "2024-12-18T11:27:50.508Z" }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876, upload-time = "2024-12-18T11:27:53.54Z" }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421, upload-time = "2024-12-18T11:27:55.409Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998, upload-time = "2024-12-18T11:27:57.252Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167, upload-time = "2024-12-18T11:27:59.146Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071, upload-time = "2024-12-18T11:28:02.625Z" }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244, upload-time = "2024-12-18T11:28:04.442Z" }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470, upload-time = "2024-12-18T11:28:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291, upload-time = "2024-12-18T11:28:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613, upload-time = "2024-12-18T11:28:13.362Z" }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355, upload-time = "2024-12-18T11:28:16.587Z" }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661, upload-time = "2024-12-18T11:28:18.407Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261, upload-time = "2024-12-18T11:28:21.471Z" }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709, upload-time = "2024-12-18T11:29:03.193Z" }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273, upload-time = "2024-12-18T11:29:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027, upload-time = "2024-12-18T11:29:07.294Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888, upload-time = "2024-12-18T11:29:09.249Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738, upload-time = "2024-12-18T11:29:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138, upload-time = "2024-12-18T11:29:16.396Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025, upload-time = "2024-12-18T11:29:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633, upload-time = "2024-12-18T11:29:23.877Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404, upload-time = "2024-12-18T11:29:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130, upload-time = "2024-12-18T11:29:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946, upload-time = "2024-12-18T11:29:31.338Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387, upload-time = "2024-12-18T11:29:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453, upload-time = "2024-12-18T11:29:35.533Z" }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186, upload-time = "2024-12-18T11:29:37.649Z" }, + { url = "https://files.pythonhosted.org/packages/43/53/13e9917fc69c0a4aea06fd63ed6a8d6cda9cf140ca9584d49c1650b0ef5e/pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", size = 1899595, upload-time = "2024-12-18T11:29:40.887Z" }, + { url = "https://files.pythonhosted.org/packages/f4/20/26c549249769ed84877f862f7bb93f89a6ee08b4bee1ed8781616b7fbb5e/pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", size = 1775010, upload-time = "2024-12-18T11:29:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/35/eb/8234e05452d92d2b102ffa1b56d801c3567e628fdc63f02080fdfc68fd5e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", size = 1830727, upload-time = "2024-12-18T11:29:46.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/df/59f915c8b929d5f61e5a46accf748a87110ba145156f9326d1a7d28912b2/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", size = 1868393, upload-time = "2024-12-18T11:29:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/d5/52/81cf4071dca654d485c277c581db368b0c95b2b883f4d7b736ab54f72ddf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", size = 2040300, upload-time = "2024-12-18T11:29:51.43Z" }, + { url = "https://files.pythonhosted.org/packages/9c/00/05197ce1614f5c08d7a06e1d39d5d8e704dc81971b2719af134b844e2eaf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", size = 2738785, upload-time = "2024-12-18T11:29:55.001Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a3/5f19bc495793546825ab160e530330c2afcee2281c02b5ffafd0b32ac05e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", size = 1996493, upload-time = "2024-12-18T11:29:57.13Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e8/e0102c2ec153dc3eed88aea03990e1b06cfbca532916b8a48173245afe60/pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", size = 1998544, upload-time = "2024-12-18T11:30:00.681Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a3/4be70845b555bd80aaee9f9812a7cf3df81550bce6dadb3cfee9c5d8421d/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", size = 2007449, upload-time = "2024-12-18T11:30:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/b779ed2480ba355c054e6d7ea77792467631d674b13d8257085a4bc7dcda/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", size = 2129460, upload-time = "2024-12-18T11:30:06.55Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f0/a6ab0681f6e95260c7fbf552874af7302f2ea37b459f9b7f00698f875492/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", size = 2159609, upload-time = "2024-12-18T11:30:09.428Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/e1059506795104349712fbca647b18b3f4a7fd541c099e6259717441e1e0/pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", size = 1819886, upload-time = "2024-12-18T11:30:11.777Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6d/df49c17f024dfc58db0bacc7b03610058018dd2ea2eaf748ccbada4c3d06/pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad", size = 1980773, upload-time = "2024-12-18T11:30:14.828Z" }, + { url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475, upload-time = "2024-12-18T11:30:18.316Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279, upload-time = "2024-12-18T11:30:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112, upload-time = "2024-12-18T11:30:23.255Z" }, + { url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780, upload-time = "2024-12-18T11:30:25.742Z" }, + { url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943, upload-time = "2024-12-18T11:30:28.036Z" }, + { url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492, upload-time = "2024-12-18T11:30:30.412Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714, upload-time = "2024-12-18T11:30:34.358Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163, upload-time = "2024-12-18T11:30:37.979Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217, upload-time = "2024-12-18T11:30:40.367Z" }, + { url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899, upload-time = "2024-12-18T11:30:42.737Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726, upload-time = "2024-12-18T11:30:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219, upload-time = "2024-12-18T11:30:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382, upload-time = "2024-12-18T11:30:51.871Z" }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159, upload-time = "2024-12-18T11:30:54.382Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331, upload-time = "2024-12-18T11:30:58.178Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467, upload-time = "2024-12-18T11:31:00.6Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797, upload-time = "2024-12-18T11:31:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839, upload-time = "2024-12-18T11:31:09.775Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861, upload-time = "2024-12-18T11:31:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582, upload-time = "2024-12-18T11:31:17.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985, upload-time = "2024-12-18T11:31:19.901Z" }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715, upload-time = "2024-12-18T11:31:22.821Z" }, + { url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733, upload-time = "2024-12-18T11:31:26.876Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375, upload-time = "2024-12-18T11:31:29.276Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307, upload-time = "2024-12-18T11:31:33.123Z" }, + { url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971, upload-time = "2024-12-18T11:31:35.755Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616, upload-time = "2024-12-18T11:31:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943, upload-time = "2024-12-18T11:31:41.853Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654, upload-time = "2024-12-18T11:31:44.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292, upload-time = "2024-12-18T11:31:48.613Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961, upload-time = "2024-12-18T11:31:52.446Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" }, + { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" }, + { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" }, + { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" }, + { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, +] + +[[package]] +name = "pyflakes" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388, upload-time = "2022-07-30T17:29:05.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116, upload-time = "2022-07-30T17:29:04.179Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pytokens" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.9'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "website-brochure-generator" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "openai" }, + { name = "python-dotenv", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "python-dotenv", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "rich" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "black", version = "25.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "flake8", version = "5.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.9.0" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=22.0.0" }, + { name = "flake8", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=0.950" }, + { name = "openai", specifier = ">=1.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "requests", specifier = ">=2.25.0" }, + { name = "rich", specifier = ">=13.0.0" }, +] +provides-extras = ["dev"] diff --git a/community-contributions/shabsi4u/Website_brochure_generator/website_brochure_generator.ipynb b/community-contributions/shabsi4u/Website_brochure_generator/website_brochure_generator.ipynb new file mode 100644 index 0000000..03cd5fa --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/website_brochure_generator.ipynb @@ -0,0 +1,939 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Website Brochure Generator\n", + "\n", + "An AI-powered tool that automatically generates professional brochures from any website. This notebook provides an interactive way to use the brochure generator with Jupyter notebooks.\n", + "\n", + "## Features\n", + "\n", + "- 🌐 **Website Analysis**: Automatically scrapes and analyzes website content\n", + "- 🤖 **AI-Powered**: Uses OpenAI GPT-4o-mini for intelligent content generation\n", + "- 📄 **Professional Output**: Generates markdown-formatted brochures\n", + "- 🌍 **Multi-Language Support**: Translate brochures to any language using AI\n", + "- ⚡ **Interactive**: Run step-by-step in Jupyter notebooks\n", + "- 🎨 **Beautiful Output**: Native Jupyter markdown rendering with HTML styling\n", + "\n", + "## Prerequisites\n", + "\n", + "- Python 3.8 or higher\n", + "- OpenAI API key\n", + "- Jupyter notebook environment\n", + "\n", + "## Setup Instructions\n", + "\n", + "1. **Get your OpenAI API key**:\n", + " - Visit [OpenAI API Keys](https://platform.openai.com/api-keys)\n", + " - Create a new API key\n", + "\n", + "2. **Set up environment variables**:\n", + " - Create a `.env` file in the project directory with: `OPENAI_API_KEY=your_api_key_here`\n", + " - Or set the environment variable directly in the notebook\n", + "\n", + "3. **Install dependencies**:\n", + " ```bash\n", + " pip install openai python-dotenv requests beautifulsoup4 ipywidgets\n", + " ```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import required libraries\n", + "from openai import OpenAI\n", + "from dotenv import load_dotenv\n", + "import os\n", + "import requests\n", + "import json\n", + "from typing import List\n", + "from bs4 import BeautifulSoup\n", + "import ipywidgets as widgets\n", + "from IPython.display import display, Markdown, HTML, clear_output\n", + "import time\n", + "\n", + "print(\"✅ All libraries imported successfully!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "Set up your OpenAI API key and configure the client.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configuration cell - Set up your OpenAI API key\n", + "def get_client_and_headers():\n", + " \"\"\"Initialize OpenAI client and headers for web scraping\"\"\"\n", + " load_dotenv(override=True)\n", + " api_key = os.getenv(\"OPENAI_API_KEY\")\n", + " \n", + " if api_key and api_key.startswith('sk-proj-') and len(api_key) > 10:\n", + " print(\"✅ API key looks good!\")\n", + " else:\n", + " print(\"⚠️ There might be a problem with your API key\")\n", + " print(\"Make sure you have set OPENAI_API_KEY in your .env file or environment variables\")\n", + "\n", + " client = OpenAI(api_key=api_key)\n", + " \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", + " return client, headers\n", + "\n", + "# Initialize the client\n", + "client, headers = get_client_and_headers()\n", + "print(\"✅ OpenAI client initialized successfully!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Core Functions\n", + "\n", + "The main functions for website analysis and brochure generation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Utility methods to display content in markdown format\n", + "def display_content(content, is_markdown=True):\n", + " \"\"\"Display content using Jupyter's display methods\"\"\"\n", + " if is_markdown:\n", + " display(Markdown(content))\n", + " else:\n", + " print(content)\n", + "\n", + "def stream_content(response, title=\"Content\"):\n", + " \"\"\"\n", + " Utility function to handle streaming content display in Jupyter\n", + " \n", + " Args:\n", + " response: OpenAI streaming response object\n", + " title (str): Title to display for the streaming content\n", + " \n", + " Returns:\n", + " str: Complete streamed content\n", + " \"\"\"\n", + " result = \"\"\n", + " \n", + " # Display title\n", + " display(HTML(f\"

{title}...

\"))\n", + " \n", + " # Create output widget for streaming\n", + " from IPython.display import clear_output\n", + " import time\n", + " \n", + " for chunk in response:\n", + " content = chunk.choices[0].delta.content or \"\"\n", + " result += content\n", + " # Print each chunk as it arrives for streaming effect\n", + " print(content, end='', flush=True)\n", + " \n", + " # Display completion message\n", + " display(HTML(f\"
{'='*50}
\"))\n", + " display(HTML(f\"
{title.upper()} COMPLETE
\"))\n", + " display(HTML(f\"
{'='*50}
\"))\n", + " \n", + " return result\n", + "\n", + "print(\"✅ Utility functions loaded!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Utility class to get the contents of a website\n", + "class Website:\n", + " def __init__(self, url):\n", + " self.url = url\n", + " self.client, self.headers = get_client_and_headers()\n", + " print(f\"🌐 Fetching content from: {url}\")\n", + " response = requests.get(url, headers=self.headers)\n", + " self.body = response.content\n", + " soup = BeautifulSoup(self.body, 'html.parser')\n", + " self.title = soup.title.string if soup.title else \"No title found\"\n", + " if soup.body:\n", + " for irrelevant in soup.body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + " self.text = soup.body.get_text(separator=\"\\n\", strip=True)\n", + " else:\n", + " self.text = \"\"\n", + " links = [link.get('href') for link in soup.find_all('a')]\n", + " self.links = [link for link in links if link]\n", + " print(f\"✅ Website analyzed: {self.title}\")\n", + "\n", + " def get_contents(self):\n", + " return f\"Webpage Title: {self.title}\\nWebpage Contents: {self.text}\\n\\n\"\n", + "\n", + "print(\"✅ Website class loaded!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# AI Prompt Functions\n", + "def get_links_system_prompt():\n", + " link_system_prompt = \"\"\"\"You are provided with a list of links found on a webpage. \\\n", + " You are able to decide which of the links would be most relevant to include in a brochure about the company. \\\n", + " Relevant links usually include: About page, or a Company page, or Careers/Jobs pages or News page\\n\"\"\"\n", + " link_system_prompt += \"Always respond in JSON exactly like this: \\n\"\n", + " link_system_prompt += \"\"\"\n", + " {\n", + " \"links\": [\n", + " {\"type\": \"\", \"url\": \"\"},\n", + " {\"type\": \"\", \"url\": \"\"}\n", + " ]\n", + " }\\n\n", + " \"\"\"\n", + " link_system_prompt += \"\"\" If no relevant links are found, return:\n", + " {\n", + " \"links\": []\n", + " }\\n\n", + " \"\"\"\n", + " link_system_prompt += \"If multiple links could map to the same type (e.g. two About pages), include the best candidate only.\\n\"\n", + "\n", + " link_system_prompt += \"You should respond in JSON as in the below examples:\\n\"\n", + " link_system_prompt += \"\"\"\n", + " ## Example 1\n", + " Input links:\n", + " - https://acme.com/about \n", + " - https://acme.com/pricing \n", + " - https://acme.com/blog \n", + " - https://acme.com/signup \n", + "\n", + " Output:\n", + " {\n", + " \"links\": [\n", + " {\"type\": \"about page\", \"url\": \"https://acme.com/about\"},\n", + " {\"type\": \"blog page\", \"url\": \"https://acme.com/blog\"},\n", + " {\"type\": \"pricing page\", \"url\": \"https://acme.com/pricing\"}\n", + " ]\n", + " }\n", + " \"\"\"\n", + " link_system_prompt += \"\"\"\n", + " ## Example 2\n", + " Input links:\n", + " - https://startup.io/ \n", + " - https://startup.io/company \n", + " - https://startup.io/careers \n", + " - https://startup.io/support \n", + "\n", + " Output:\n", + " {\n", + " \"links\": [\n", + " {\"type\": \"company page\", \"url\": \"https://startup.io/company\"},\n", + " {\"type\": \"careers page\", \"url\": \"https://startup.io/careers\"}\n", + " ]\n", + " }\n", + " \"\"\"\n", + " link_system_prompt += \"\"\"\n", + " ## Example 3\n", + " Input links:\n", + " - https://coolapp.xyz/login \n", + " - https://coolapp.xyz/random \n", + "\n", + " Output:\n", + " {\n", + " \"links\": []\n", + " }\n", + " \"\"\"\n", + " return link_system_prompt\n", + "\n", + "def get_links_user_prompt(website):\n", + " user_prompt = f\"Here is the list of links on the website of {website.url} - \"\n", + " user_prompt += \"please decide which of these are relevant web links for a brochure about the company, respond with the full https URL in JSON format. \\n\"\n", + " user_prompt += \"Do not include Terms of Service, Privacy, email links.\\n\"\n", + " user_prompt += \"Links (some might be relative links):\\n\"\n", + " user_prompt += \"\\n\".join(website.links)\n", + " return user_prompt\n", + "\n", + "def get_brochure_system_prompt():\n", + " brochure_system_prompt = \"\"\"\n", + " You are an assistant that analyzes the contents of several relevant pages from a company website \\\n", + " and creates a short brochure about the company for prospective customers, investors and recruits. Respond in markdown.\n", + " Include details of company culture, customers and careers/jobs if you have the information.\n", + " \"\"\"\n", + " return brochure_system_prompt\n", + "\n", + "def get_brochure_user_prompt(url):\n", + " user_prompt = f\"You are looking at a company details of: {url}\\n\"\n", + " user_prompt += f\"Here are the contents of its landing page and other relevant pages; use this information to build a short brochure of the company in markdown.\\n\"\n", + " user_prompt += get_details_for_brochure(url)\n", + " user_prompt = user_prompt[:15000] # Truncate if more than 15,000 characters\n", + " return user_prompt\n", + "\n", + "def get_translation_system_prompt(target_language):\n", + " translation_system_prompt = f\"You are a professional translator specializing in business and marketing content. \\\n", + " Translate the provided brochure to {target_language} while maintaining all formatting and professional tone.\"\n", + " return translation_system_prompt\n", + "\n", + "def get_translation_user_prompt(original_brochure, target_language):\n", + " translation_prompt = f\"\"\"\n", + " You are a professional translator. Please translate the following brochure content to {target_language}.\n", + " \n", + " Important guidelines:\n", + " - Maintain the markdown formatting exactly as it appears\n", + " - Keep all headers, bullet points, and structure intact\n", + " - Translate the content naturally and professionally\n", + " - Preserve any company names, product names, or proper nouns unless they have established translations\n", + " - Maintain the professional tone and marketing style\n", + " \n", + " Brochure content to translate:\n", + " {original_brochure}\n", + " \"\"\"\n", + " return translation_prompt\n", + "\n", + "print(\"✅ AI prompt functions loaded!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Core Brochure Generation Functions\n", + "def get_links(url):\n", + " \"\"\"Get relevant links from a website using AI analysis\"\"\"\n", + " website = Website(url)\n", + " response = website.client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": get_links_system_prompt()},\n", + " {\"role\": \"user\", \"content\": get_links_user_prompt(website)}\n", + " ],\n", + " response_format={\"type\": \"json_object\"}\n", + " )\n", + " result = response.choices[0].message.content\n", + " print(\"🔗 Found relevant links:\", result)\n", + " return json.loads(result)\n", + "\n", + "def get_details_for_brochure(url):\n", + " \"\"\"Get comprehensive details from website and relevant pages\"\"\"\n", + " website = Website(url)\n", + " result = \"Landing page:\\n\"\n", + " result += website.get_contents()\n", + " links = get_links(url)\n", + " print(\"📄 Analyzing additional pages...\")\n", + " for link in links[\"links\"]:\n", + " result += f\"\\n\\n{link['type']}\\n\"\n", + " result += Website(link[\"url\"]).get_contents()\n", + " return result\n", + "\n", + "def create_brochure(url):\n", + " \"\"\"Create a brochure from a website URL\"\"\"\n", + " website = Website(url)\n", + " print(\"🤖 Generating brochure with AI...\")\n", + " response = website.client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": get_brochure_system_prompt()},\n", + " {\"role\": \"user\", \"content\": get_brochure_user_prompt(url)}\n", + " ]\n", + " )\n", + " result = response.choices[0].message.content\n", + " display_content(result, is_markdown=True)\n", + " return result\n", + "\n", + "def stream_brochure(url):\n", + " \"\"\"Create a brochure with streaming output\"\"\"\n", + " website = Website(url)\n", + " print(\"🤖 Generating brochure with streaming output...\")\n", + " response = website.client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": get_brochure_system_prompt()},\n", + " {\"role\": \"user\", \"content\": get_brochure_user_prompt(url)}\n", + " ],\n", + " stream=True\n", + " )\n", + " \n", + " # Use the reusable streaming utility function\n", + " result = stream_content(response, \"Generating brochure\")\n", + " return result\n", + "\n", + "print(\"✅ Core brochure generation functions loaded!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Translation Functions\n", + "def translate_brochure(url, target_language=\"Spanish\", stream_mode=False):\n", + " \"\"\"\n", + " Generate a brochure and translate it to the target language\n", + " \n", + " Args:\n", + " url (str): The website URL to generate brochure from\n", + " target_language (str): The target language for translation (default: \"Spanish\")\n", + " stream_mode (bool): Whether to use streaming output (default: False)\n", + " \n", + " Returns:\n", + " str: Translated brochure content\n", + " \"\"\"\n", + " # First generate the original brochure\n", + " print(f\"🌍 Generating brochure and translating to {target_language}...\")\n", + " original_brochure = create_brochure(url)\n", + " \n", + " # Get translation prompts\n", + " translation_system_prompt = get_translation_system_prompt(target_language)\n", + " translation_user_prompt = get_translation_user_prompt(original_brochure, target_language)\n", + " \n", + " # Get OpenAI client\n", + " website = Website(url)\n", + " \n", + " if stream_mode:\n", + " # Generate translation using OpenAI with streaming\n", + " response = website.client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": translation_system_prompt},\n", + " {\"role\": \"user\", \"content\": translation_user_prompt}\n", + " ],\n", + " stream=True\n", + " )\n", + " \n", + " # Use the reusable streaming utility function\n", + " translated_brochure = stream_content(response, f\"Translating brochure to {target_language}\")\n", + " else:\n", + " # Generate translation using OpenAI with complete output\n", + " response = website.client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": translation_system_prompt},\n", + " {\"role\": \"user\", \"content\": translation_user_prompt}\n", + " ]\n", + " )\n", + " \n", + " translated_brochure = response.choices[0].message.content\n", + " \n", + " # Display the translated content\n", + " display_content(translated_brochure, is_markdown=True)\n", + " \n", + " return translated_brochure\n", + "\n", + "print(\"✅ Translation functions loaded!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interactive Examples\n", + "\n", + "Now let's try generating brochures for some example websites. You can run these cells to see the brochure generator in action!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example 1: Generate a brochure for a sample website\n", + "# You can change this URL to any website you want to analyze\n", + "\n", + "sample_url = \"https://openai.com\" # Change this to any website you want to analyze\n", + "\n", + "print(f\"🚀 Generating brochure for: {sample_url}\")\n", + "print(\"=\" * 60)\n", + "\n", + "# Generate the brochure\n", + "brochure = create_brochure(sample_url)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example 2: Generate a brochure with streaming output\n", + "# This shows the brochure being generated in real-time\n", + "\n", + "streaming_url = \"https://anthropic.com\" # Change this to any website you want to analyze\n", + "\n", + "print(f\"🚀 Generating brochure with streaming for: {streaming_url}\")\n", + "print(\"=\" * 60)\n", + "\n", + "# Generate the brochure with streaming\n", + "streaming_brochure = stream_brochure(streaming_url)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example 3: Generate and translate a brochure\n", + "# This creates a brochure and then translates it to another language\n", + "\n", + "translation_url = \"https://huggingface.co\" # Change this to any website you want to analyze\n", + "target_language = \"Spanish\" # Change this to any language you want\n", + "\n", + "print(f\"🚀 Generating and translating brochure for: {translation_url}\")\n", + "print(f\"🌍 Target language: {target_language}\")\n", + "print(\"=\" * 60)\n", + "\n", + "# Generate and translate the brochure\n", + "translated_brochure = translate_brochure(translation_url, target_language, stream_mode=False)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interactive Widget Interface\n", + "\n", + "Use the widgets below to interactively generate brochures for any website!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Interactive Widget Interface\n", + "import ipywidgets as widgets\n", + "from IPython.display import display, clear_output\n", + "\n", + "# Create widgets\n", + "url_input = widgets.Text(\n", + " value='https://openai.com',\n", + " placeholder='Enter website URL (e.g., https://example.com)',\n", + " description='Website URL:',\n", + " style={'description_width': 'initial'},\n", + " layout=widgets.Layout(width='500px')\n", + ")\n", + "\n", + "language_dropdown = widgets.Dropdown(\n", + " options=['English', 'Spanish', 'French', 'German', 'Chinese', 'Japanese', 'Portuguese', 'Italian'],\n", + " value='English',\n", + " description='Language:',\n", + " style={'description_width': 'initial'}\n", + ")\n", + "\n", + "stream_checkbox = widgets.Checkbox(\n", + " value=False,\n", + " description='Use streaming output',\n", + " style={'description_width': 'initial'}\n", + ")\n", + "\n", + "translate_checkbox = widgets.Checkbox(\n", + " value=False,\n", + " description='Translate brochure',\n", + " style={'description_width': 'initial'}\n", + ")\n", + "\n", + "generate_button = widgets.Button(\n", + " description='Generate Brochure',\n", + " button_style='success',\n", + " icon='rocket'\n", + ")\n", + "\n", + "output_area = widgets.Output()\n", + "\n", + "def on_generate_clicked(b):\n", + " with output_area:\n", + " clear_output(wait=True)\n", + " url = url_input.value.strip()\n", + " \n", + " if not url:\n", + " print(\"❌ Please enter a valid URL\")\n", + " return\n", + " \n", + " if not url.startswith(('http://', 'https://')):\n", + " url = 'https://' + url\n", + " \n", + " print(f\"🚀 Generating brochure for: {url}\")\n", + " print(\"=\" * 60)\n", + " \n", + " try:\n", + " if translate_checkbox.value:\n", + " # Generate and translate\n", + " result = translate_brochure(url, language_dropdown.value, stream_mode=stream_checkbox.value)\n", + " else:\n", + " # Generate only\n", + " if stream_checkbox.value:\n", + " result = stream_brochure(url)\n", + " else:\n", + " result = create_brochure(url)\n", + " \n", + " print(\"\\n✅ Brochure generation completed!\")\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error generating brochure: {str(e)}\")\n", + " print(\"Please check your API key and internet connection.\")\n", + "\n", + "generate_button.on_click(on_generate_clicked)\n", + "\n", + "# Display widgets\n", + "print(\"🎯 Interactive Brochure Generator\")\n", + "print(\"Enter a website URL and click 'Generate Brochure' to create a professional brochure!\")\n", + "print()\n", + "\n", + "display(url_input)\n", + "display(widgets.HBox([language_dropdown, stream_checkbox, translate_checkbox]))\n", + "display(generate_button)\n", + "display(output_area)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Usage Examples\n", + "\n", + "Here are some advanced examples showing different ways to use the brochure generator.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Advanced Example 1: Analyze multiple websites and compare\n", + "websites_to_analyze = [\n", + " \"https://openai.com\",\n", + " \"https://anthropic.com\", \n", + " \"https://huggingface.co\"\n", + "]\n", + "\n", + "print(\"🔍 Analyzing multiple websites...\")\n", + "print(\"=\" * 60)\n", + "\n", + "brochures = {}\n", + "for url in websites_to_analyze:\n", + " print(f\"\\n📊 Generating brochure for: {url}\")\n", + " try:\n", + " brochure = create_brochure(url)\n", + " brochures[url] = brochure\n", + " print(f\"✅ Successfully generated brochure for {url}\")\n", + " except Exception as e:\n", + " print(f\"❌ Failed to generate brochure for {url}: {str(e)}\")\n", + " \n", + " print(\"-\" * 40)\n", + "\n", + "print(f\"\\n🎉 Generated {len(brochures)} brochures successfully!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Advanced Example 2: Generate brochures in multiple languages\n", + "target_website = \"https://openai.com\" # Change this to any website\n", + "languages = [\"Spanish\", \"French\", \"German\", \"Chinese\"]\n", + "\n", + "print(f\"🌍 Generating brochures in multiple languages for: {target_website}\")\n", + "print(\"=\" * 60)\n", + "\n", + "multilingual_brochures = {}\n", + "for language in languages:\n", + " print(f\"\\n🔄 Translating to {language}...\")\n", + " try:\n", + " translated_brochure = translate_brochure(target_website, language, stream_mode=False)\n", + " multilingual_brochures[language] = translated_brochure\n", + " print(f\"✅ Successfully translated to {language}\")\n", + " except Exception as e:\n", + " print(f\"❌ Failed to translate to {language}: {str(e)}\")\n", + " \n", + " print(\"-\" * 40)\n", + "\n", + "print(f\"\\n🎉 Generated brochures in {len(multilingual_brochures)} languages!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom Functions\n", + "\n", + "Create your own custom functions for specific use cases.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Custom Function: Save brochure to file\n", + "def save_brochure_to_file(brochure_content, filename, url):\n", + " \"\"\"Save brochure content to a markdown file\"\"\"\n", + " try:\n", + " with open(filename, 'w', encoding='utf-8') as f:\n", + " f.write(f\"# Brochure for {url}\\n\\n\")\n", + " f.write(f\"Generated on: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n\\n\")\n", + " f.write(\"---\\n\\n\")\n", + " f.write(brochure_content)\n", + " print(f\"✅ Brochure saved to: {filename}\")\n", + " return True\n", + " except Exception as e:\n", + " print(f\"❌ Error saving brochure: {str(e)}\")\n", + " return False\n", + "\n", + "# Custom Function: Generate brochure with custom analysis\n", + "def generate_custom_brochure(url, focus_areas=None):\n", + " \"\"\"Generate a brochure with focus on specific areas\"\"\"\n", + " if focus_areas is None:\n", + " focus_areas = [\"company overview\", \"products\", \"culture\", \"careers\"]\n", + " \n", + " website = Website(url)\n", + " \n", + " # Custom system prompt with focus areas\n", + " custom_system_prompt = f\"\"\"\n", + " You are an assistant that analyzes website content and creates a professional brochure.\n", + " Focus specifically on these areas: {', '.join(focus_areas)}.\n", + " Create a markdown brochure that emphasizes these aspects for prospective customers, investors and recruits.\n", + " \"\"\"\n", + " \n", + " response = website.client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": custom_system_prompt},\n", + " {\"role\": \"user\", \"content\": get_brochure_user_prompt(url)}\n", + " ]\n", + " )\n", + " \n", + " result = response.choices[0].message.content\n", + " display_content(result, is_markdown=True)\n", + " return result\n", + "\n", + "# Custom Function: Quick website analysis\n", + "def quick_website_analysis(url):\n", + " \"\"\"Perform a quick analysis of a website without generating full brochure\"\"\"\n", + " website = Website(url)\n", + " \n", + " analysis = f\"\"\"\n", + " # Quick Website Analysis: {url}\n", + " \n", + " **Title:** {website.title}\n", + " **Total Links Found:** {len(website.links)}\n", + " **Content Length:** {len(website.text)} characters\n", + " \n", + " ## Sample Content (first 500 characters):\n", + " {website.text[:500]}...\n", + " \n", + " ## All Links:\n", + " {chr(10).join(website.links[:10])} # Show first 10 links\n", + " \"\"\"\n", + " \n", + " display_content(analysis, is_markdown=True)\n", + " return analysis\n", + "\n", + "print(\"✅ Custom functions loaded!\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage Examples with Custom Functions\n", + "\n", + "Try these examples with the custom functions we just created.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Quick website analysis\n", + "test_url = \"https://openai.com\" # Change this to any website\n", + "\n", + "print(\"🔍 Performing quick website analysis...\")\n", + "print(\"=\" * 50)\n", + "\n", + "quick_analysis = quick_website_analysis(test_url)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Generate custom brochure with specific focus\n", + "custom_url = \"https://anthropic.com\" # Change this to any website\n", + "focus_areas = [\"AI safety\", \"research\", \"products\", \"team\"] # Custom focus areas\n", + "\n", + "print(\"🎯 Generating custom brochure with specific focus...\")\n", + "print(f\"Focus areas: {', '.join(focus_areas)}\")\n", + "print(\"=\" * 50)\n", + "\n", + "custom_brochure = generate_custom_brochure(custom_url, focus_areas)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Generate brochure and save to file\n", + "save_url = \"https://huggingface.co\" # Change this to any website\n", + "\n", + "print(\"💾 Generating brochure and saving to file...\")\n", + "print(\"=\" * 50)\n", + "\n", + "# Generate brochure\n", + "brochure_content = create_brochure(save_url)\n", + "\n", + "# Save to file\n", + "filename = f\"brochure_{save_url.replace('https://', '').replace('/', '_')}.md\"\n", + "save_success = save_brochure_to_file(brochure_content, filename, save_url)\n", + "\n", + "if save_success:\n", + " print(f\"📁 You can find the saved brochure in: {filename}\")\n", + "else:\n", + " print(\"❌ Failed to save brochure to file\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Troubleshooting and Tips\n", + "\n", + "### Common Issues and Solutions\n", + "\n", + "1. **API Key Issues**\n", + " - Make sure your OpenAI API key is set in the `.env` file\n", + " - Verify your API key has sufficient credits\n", + " - Check that the key starts with `sk-proj-`\n", + "\n", + "2. **Website Scraping Issues**\n", + " - Some websites may block automated requests\n", + " - Try different websites if one fails\n", + " - The tool uses a standard User-Agent header to avoid basic blocking\n", + "\n", + "3. **Memory Issues**\n", + " - Large websites may consume significant memory\n", + " - The tool truncates content to 15,000 characters to manage this\n", + "\n", + "4. **Rate Limiting**\n", + " - OpenAI has rate limits on API calls\n", + " - If you hit limits, wait a few minutes before trying again\n", + "\n", + "### Tips for Better Results\n", + "\n", + "1. **Choose Good Websites**\n", + " - Websites with clear About, Products, and Careers pages work best\n", + " - Avoid websites that are mostly images or require JavaScript\n", + "\n", + "2. **Use Streaming for Long Content**\n", + " - Enable streaming for better user experience with long brochures\n", + " - Streaming shows progress in real-time\n", + "\n", + "3. **Custom Focus Areas**\n", + " - Use the custom brochure function to focus on specific aspects\n", + " - This can help generate more targeted content\n", + "\n", + "4. **Save Your Work**\n", + " - Use the save function to keep brochures for later reference\n", + " - Files are saved in markdown format for easy editing\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This Jupyter notebook provides a comprehensive interface for the Website Brochure Generator. You can:\n", + "\n", + "- ✅ Generate professional brochures from any website\n", + "- ✅ Translate brochures to multiple languages\n", + "- ✅ Use interactive widgets for easy operation\n", + "- ✅ Save brochures to files for later use\n", + "- ✅ Perform quick website analysis\n", + "- ✅ Create custom brochures with specific focus areas\n", + "- ✅ Generate brochures with streaming output for real-time feedback\n", + "\n", + "### Next Steps\n", + "\n", + "1. **Try the Interactive Widget**: Use the widget interface above to generate brochures for your favorite websites\n", + "2. **Experiment with Different URLs**: Test the tool with various types of websites\n", + "3. **Explore Translation Features**: Generate brochures in different languages\n", + "4. **Save Your Work**: Use the save function to keep your generated brochures\n", + "5. **Customize Focus Areas**: Create brochures tailored to specific aspects of companies\n", + "\n", + "### Support\n", + "\n", + "For issues and questions:\n", + "- Check the troubleshooting section above\n", + "- Verify your OpenAI API key is properly configured\n", + "- Ensure you have a stable internet connection\n", + "- Try different websites if one fails\n", + "\n", + "Happy brochure generating! 🚀\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/community-contributions/shabsi4u/Website_brochure_generator/website_brochure_generator.py b/community-contributions/shabsi4u/Website_brochure_generator/website_brochure_generator.py new file mode 100644 index 0000000..88f75c4 --- /dev/null +++ b/community-contributions/shabsi4u/Website_brochure_generator/website_brochure_generator.py @@ -0,0 +1,356 @@ +from openai import OpenAI +from dotenv import load_dotenv +import os +import requests +import json +from typing import List +from bs4 import BeautifulSoup + +# Rich library for beautiful terminal markdown rendering +from rich.console import Console +from rich.markdown import Markdown as RichMarkdown + +def get_client_and_headers(): + load_dotenv(override=True) + api_key = os.getenv("OPENAI_API_KEY") + if api_key and api_key.startswith('sk-proj-') and len(api_key)>10: + # print("API key looks good so far") + pass + else: + print("There might be a problem with your API key") + + client = OpenAI(api_key=api_key) + + headers = { + "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" + } + return client, headers + +# Utility methods to display content in markdown format +def print_markdown_terminal(text): + """Print markdown-formatted text to terminal with beautiful formatting using Rich""" + console = Console() + console.print(RichMarkdown(text)) + +def display_content(content, is_markdown=True): + """Display content using Rich formatting""" + if is_markdown: + print_markdown_terminal(content) + else: + print(content) + +def stream_content(response, title="Content"): + """ + Utility function to handle streaming content display using Rich + + Args: + response: OpenAI streaming response object + title (str): Title to display for the streaming content + + Returns: + str: Complete streamed content + """ + result = "" + console = Console() + + # Terminal streaming with real-time output using Rich + console.print(f"\n[bold blue]{title}...[/bold blue]\n") + for chunk in response: + content = chunk.choices[0].delta.content or "" + result += content + # Print each chunk as it arrives for streaming effect + print(content, end='', flush=True) + console.print(f"\n\n[bold green]{'='*50}[/bold green]") + console.print(f"[bold green]{title.upper()} COMPLETE[/bold green]") + console.print(f"[bold green]{'='*50}[/bold green]") + + return result + +# Utility class to get the contents of a website +class Website: + def __init__(self, url): + self.url = url + self.client, self.headers = get_client_and_headers() + response = requests.get(url, headers=self.headers) + self.body = response.content + soup = BeautifulSoup(self.body, 'html.parser') + self.title = soup.title.string if soup.title else "No title found" + if soup.body: + for irrelevant in soup.body(["script", "style", "img", "input"]): + irrelevant.decompose() + self.text = soup.body.get_text(separator="\n", strip=True) + else: + self.text = "" + links = [link.get('href') for link in soup.find_all('a')] + self.links = [link for link in links if link] + + def get_contents(self): + return f"Webpage Title: {self.title}\nWebpage Contents: {self.text}\n\n" + +def get_links_system_prompt(): + link_system_prompt = """"You are provided with a list of links found on a webpage. \ + You are able to decide which of the links would be most relevant to include in a brochure about the company. \ + Relevant links usually include: About page, or a Company page, or Careers/Jobs pages or News page\n""" + link_system_prompt += "Always respond in JSON exactly like this: \n" + link_system_prompt += """ + { + "links": [ + {"type": "", "url": ""}, + {"type": "", "url": ""} + ] + }\n + """ + link_system_prompt += """ If no relevant links are found, return: + { + "links": [] + }\n + """ + link_system_prompt += "If multiple links could map to the same type (e.g. two About pages), include the best candidate only.\n" + + link_system_prompt += "You should respond in JSON as in the below examples:\n" + link_system_prompt += """ + ## Example 1 + Input links: + - https://acme.com/about + - https://acme.com/pricing + - https://acme.com/blog + - https://acme.com/signup + + Output: + { + "links": [ + {"type": "about page", "url": "https://acme.com/about"}, + {"type": "blog page", "url": "https://acme.com/blog"}, + {"type": "pricing page", "url": "https://acme.com/pricing"} + ] + } + """ + link_system_prompt += """ + ## Example 2 + Input links: + - https://startup.io/ + - https://startup.io/company + - https://startup.io/careers + - https://startup.io/support + + Output: + { + "links": [ + {"type": "company page", "url": "https://startup.io/company"}, + {"type": "careers page", "url": "https://startup.io/careers"} + ] + } + """ + link_system_prompt += """ + ## Example 3 + Input links: + - https://coolapp.xyz/login + - https://coolapp.xyz/random + + Output: + { + "links": [] + } + """ + return link_system_prompt + +def get_links_user_prompt(website): + user_prompt = f"Here is the list of links on the website of {website.url} - " + user_prompt += "please decide which of these are relevant web links for a brochure about the company, respond with the full https URL in JSON format. \n" + user_prompt += "Do not include Terms of Service, Privacy, email links.\n" + user_prompt += "Links (some might be relative links):\n" + user_prompt += "\n".join(website.links) + return user_prompt + +def get_brochure_system_prompt(): + brochure_system_prompt = """ + You are an assistant that analyzes the contents of several relevant pages from a company website \ + and creates a short brochure about the company for prospective customers, investors and recruits. Respond in markdown. + Include details of company culture, customers and careers/jobs if you have the information. + """ + return brochure_system_prompt + +def get_brochure_user_prompt(url): + user_prompt = f"You are looking at a company details of: {url}\n" + user_prompt += f"Here are the contents of its landing page and other relevant pages; use this information to build a short brochure of the company in markdown.\n" + user_prompt += get_details_for_brochure(url) + user_prompt = user_prompt[:15000] # Truncate if more than 15,000 characters + return user_prompt + +def get_translation_system_prompt(target_language): + translation_system_prompt = f"You are a professional translator specializing in business and marketing content. \ + Translate the provided brochure to {target_language} while maintaining all formatting and professional tone." + return translation_system_prompt + +def get_translation_user_prompt(original_brochure, target_language): + translation_prompt = f""" + You are a professional translator. Please translate the following brochure content to {target_language}. + + Important guidelines: + - Maintain the markdown formatting exactly as it appears + - Keep all headers, bullet points, and structure intact + - Translate the content naturally and professionally + - Preserve any company names, product names, or proper nouns unless they have established translations + - Maintain the professional tone and marketing style + + Brochure content to translate: + {original_brochure} + """ + return translation_prompt + +def get_links(url): + website = Website(url) + response = website.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": get_links_system_prompt()}, + {"role": "user", "content": get_links_user_prompt(website)} + ], + response_format={"type": "json_object"} + ) + result = response.choices[0].message.content + print("get_links:", result) + return json.loads(result) + +def get_details_for_brochure(url): + website = Website(url) + result = "Landing page:\n" + result += website.get_contents() + links = get_links(url) + print("Found links:", links) + for link in links["links"]: + result += f"\n\n{link['type']}\n" + result += Website(link["url"]).get_contents() + return result + +def create_brochure(url): + website = Website(url) + response = website.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": get_brochure_system_prompt()}, + {"role": "user", "content": get_brochure_user_prompt(url)} + ] + ) + result = response.choices[0].message.content + display_content(result, is_markdown=True) + return result + +def stream_brochure(url): + website = Website(url) + response = website.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": get_brochure_system_prompt()}, + {"role": "user", "content": get_brochure_user_prompt(url)} + ], + stream=True + ) + + # Use the reusable streaming utility function + result = stream_content(response, "Generating brochure") + return result + +def translate_brochure(url, target_language="Spanish", stream_mode=False): + """ + Generate a brochure and translate it to the target language + + Args: + url (str): The website URL to generate brochure from + target_language (str): The target language for translation (default: "Spanish") + stream_mode (bool): Whether to use streaming output (default: False) + + Returns: + str: Translated brochure content + """ + # First generate the original brochure + original_brochure = create_brochure(url) + + # Get translation prompts + translation_system_prompt = get_translation_system_prompt(target_language) + translation_user_prompt = get_translation_user_prompt(original_brochure, target_language) + + # Get OpenAI client + website = Website(url) + + if stream_mode: + # Generate translation using OpenAI with streaming + response = website.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": translation_system_prompt}, + {"role": "user", "content": translation_user_prompt} + ], + stream=True + ) + + # Use the reusable streaming utility function + translated_brochure = stream_content(response, f"Translating brochure to {target_language}") + else: + # Generate translation using OpenAI with complete output + response = website.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": translation_system_prompt}, + {"role": "user", "content": translation_user_prompt} + ] + ) + + translated_brochure = response.choices[0].message.content + + # Display the translated content + display_content(translated_brochure, is_markdown=True) + + return translated_brochure + + +# Main function for terminal usage +def main(): + """Main function for running the brochure generator from terminal""" + import sys + + if len(sys.argv) != 2: + console = Console() + console.print("[bold red]Usage:[/bold red] python website_brochure_generator.py ") + console.print("[bold blue]Example:[/bold blue] python website_brochure_generator.py https://example.com") + sys.exit(1) + + url = sys.argv[1] + console = Console() + + console.print(f"[bold green]Generating brochure for:[/bold green] {url}") + console.print("\n[bold yellow]Choose display mode:[/bold yellow]") + console.print("1. Complete output (display all at once)") + console.print("2. Stream output (real-time generation)") + + display_choice = input("\nEnter choice (1 or 2): ").strip() + + # Generate brochure based on display choice + if display_choice == "1": + result = create_brochure(url) + elif display_choice == "2": + result = stream_brochure(url) + else: + console.print("[bold red]Invalid choice. Using default: complete output[/bold red]") + result = create_brochure(url) + + # Ask if user wants translation + console.print("\n[bold yellow]Translation options:[/bold yellow]") + console.print("1. No translation (original only)") + console.print("2. Translate to another language") + + translation_choice = input("\nEnter choice (1 or 2): ").strip() + + if translation_choice == "2": + target_language = input("Enter target language (e.g., Spanish, French, German, Chinese): ").strip() + if not target_language: + target_language = "Spanish" + + # Pass the stream mode based on the display choice + stream_mode = (display_choice == "2") + translate_brochure(url, target_language, stream_mode=stream_mode) + else: + pass + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/week1/community-contributions/01_webpage_summarizer.ipynb b/week1/community-contributions/01_webpage_summarizer.ipynb index f8be204..daa7456 100644 --- a/week1/community-contributions/01_webpage_summarizer.ipynb +++ b/week1/community-contributions/01_webpage_summarizer.ipynb @@ -335,7 +335,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -349,7 +349,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/week1/community-contributions/Shriyash_Patil_WebscrapperDay1.ipynb b/week1/community-contributions/Shriyash_Patil_WebscrapperDay1.ipynb new file mode 100644 index 0000000..4f2b0e3 --- /dev/null +++ b/week1/community-contributions/Shriyash_Patil_WebscrapperDay1.ipynb @@ -0,0 +1,302 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "id": "fafbdb1f-6ecf-4fee-a1d2-80c6f33b556d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: selenium in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (4.36.0)\n", + "Requirement already satisfied: urllib3<3.0,>=2.5.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from urllib3[socks]<3.0,>=2.5.0->selenium) (2.5.0)\n", + "Requirement already satisfied: trio<1.0,>=0.30.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from selenium) (0.31.0)\n", + "Requirement already satisfied: trio-websocket<1.0,>=0.12.2 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from selenium) (0.12.2)\n", + "Requirement already satisfied: certifi>=2025.6.15 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from selenium) (2025.8.3)\n", + "Requirement already satisfied: typing_extensions<5.0,>=4.14.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from selenium) (4.15.0)\n", + "Requirement already satisfied: websocket-client<2.0,>=1.8.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from selenium) (1.8.0)\n", + "Requirement already satisfied: attrs>=23.2.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from trio<1.0,>=0.30.0->selenium) (25.3.0)\n", + "Requirement already satisfied: sortedcontainers in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from trio<1.0,>=0.30.0->selenium) (2.4.0)\n", + "Requirement already satisfied: idna in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from trio<1.0,>=0.30.0->selenium) (3.10)\n", + "Requirement already satisfied: outcome in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from trio<1.0,>=0.30.0->selenium) (1.3.0.post0)\n", + "Requirement already satisfied: sniffio>=1.3.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from trio<1.0,>=0.30.0->selenium) (1.3.1)\n", + "Requirement already satisfied: wsproto>=0.14 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from trio-websocket<1.0,>=0.12.2->selenium) (1.2.0)\n", + "Requirement already satisfied: pysocks!=1.5.7,<2.0,>=1.5.6 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from urllib3[socks]<3.0,>=2.5.0->selenium) (1.7.1)\n", + "Requirement already satisfied: h11<1,>=0.9.0 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from wsproto>=0.14->trio-websocket<1.0,>=0.12.2->selenium) (0.16.0)\n", + "Requirement already satisfied: webdriver-manager in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (4.0.2)\n", + "Requirement already satisfied: requests in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from webdriver-manager) (2.32.5)\n", + "Requirement already satisfied: python-dotenv in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from webdriver-manager) (1.1.1)\n", + "Requirement already satisfied: packaging in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from webdriver-manager) (25.0)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from requests->webdriver-manager) (3.4.3)\n", + "Requirement already satisfied: idna<4,>=2.5 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from requests->webdriver-manager) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from requests->webdriver-manager) (2.5.0)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /opt/anaconda3/envs/llms/lib/python3.11/site-packages (from requests->webdriver-manager) (2025.8.3)\n" + ] + } + ], + "source": [ + "!pip install selenium\n", + "!pip install webdriver-manager" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "fc4283fd-504a-43fa-a92b-7b54c76b39a0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "API key found and looks good so far!\n" + ] + }, + { + "data": { + "text/markdown": [ + "Sure! Here's a parody Twitter thread based on the homepage of EdwardDonner.com. Each tweet captures the tone and structure of the site in a sarcastic manner.\n", + "\n", + "---\n", + "\n", + "**1/6 🥳 Welcome to Edward Donner!** \n", + "Where we officially celebrate the mundane and demonstrate how to make everything sound like it's a life-changing experience. Get your \"meh\" ready, because the excitement is practically oozing out of our non-existent product descriptions! \n", + "\n", + "---\n", + "\n", + "**2/6 🌟 Our \"Mission\":** \n", + "To show you that while you shop, there's a slight chance you might save a couple of bucks! Because why just shop when you can do it with absolutely zero risk of fun or spontaneity? We take the thrill out of thrifting—you're welcome!\n", + "\n", + "---\n", + "\n", + "**3/6 💪 What We Offer:** \n", + "Oh, just your run-of-the-mill assortment of \"high-quality\" products that we’ve totally not pulled from the clearance bin. From must-have items to things you didn’t ask for but we'll sell you anyway. It's like a treasure hunt, but without the treasure!\n", + "\n", + "---\n", + "\n", + "**4/6 📦 Our Customers:** \n", + "We love to brag about our fictitious wide-eyed customers who are THRILLED to have stumbled upon us. They literally danced in joy—probably because they mistook our site for a disco party. Who needs real satisfaction when you’ve got buyer’s remorse?\n", + "\n", + "---\n", + "\n", + "**5/6 🎉 Our Commitment:** \n", + "“Convenience is key!” they say. So we’ve made it super easy to shop from your couch without even the slightest hint of real fulfillment. You can binge on shopping while scrolling through cat videos—multitasking at its finest! 🙌\n", + "\n", + "---\n", + "\n", + "**6/6 💼 Join Us Today!** \n", + "Dive on in, the water’s lukewarm! Sign up for updates and prepare for thrill—like, remember checking your email? It’s like getting a surprise tax form in your inbox, only less exciting! Don’t miss out on treasure, folks! 😂 #LivingTheDream\n", + "\n", + "--- \n", + "\n", + "Feel free to share or adjust the humor to your liking!" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "I'm sorry, but I can't access external websites directly, including the one you provided. However, if you can share some of the content or main points from the website, I can help you craft a light and witty parody based on that information!" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#import\n", + "\n", + "import os\n", + "import requests\n", + "import time\n", + "from dotenv import load_dotenv\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display\n", + "from openai import OpenAI\n", + "from selenium import webdriver\n", + "from selenium.webdriver.chrome.service import Service\n", + "from selenium.webdriver.chrome.options import Options\n", + "from selenium.webdriver.common.by import By\n", + "from selenium.webdriver.support.ui import WebDriverWait\n", + "from selenium.webdriver.support import expected_conditions as EC\n", + "from webdriver_manager.chrome import ChromeDriverManager\n", + "\n", + "\n", + "# Get the api key\n", + "load_dotenv(override=True)\n", + "api_key = os.getenv('OPENAI_API_KEY')\n", + "\n", + "# Check the key\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!\")\n", + "\n", + "#create an object of OpenAI\n", + "openai = OpenAI()\n", + "\n", + "\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", + " def __init__(self, url, headless=True, chrome_binary=None, wait_seconds=10):\n", + " \"\"\"\n", + " Create this Website object from the given url using Selenium WebDriver.\n", + " Uses webdriver-manager to fetch a compatible chromedriver automatically.\n", + " Parameters:\n", + " - url: target URL\n", + " - headless: run chrome headless (True/False)\n", + " - chrome_binary: optional path to chrome/chromium binary (if not in PATH)\n", + " - wait_seconds: timeout for waiting page load/dynamic content\n", + " \"\"\"\n", + " self.url = url\n", + " options = Options()\n", + "\n", + " # headless or visible browser\n", + " if headless:\n", + " options.add_argument(\"--headless=new\") # use new headless flag where supported\n", + " options.add_argument(\"--no-sandbox\")\n", + " options.add_argument(\"--disable-dev-shm-usage\") # helpful in containers\n", + " options.add_argument(\"--disable-gpu\")\n", + " # some sites detect automation; these flags may help\n", + " options.add_argument(\"--disable-blink-features=AutomationControlled\")\n", + " options.add_experimental_option(\"excludeSwitches\", [\"enable-automation\"])\n", + " options.add_experimental_option('useAutomationExtension', False)\n", + "\n", + " # If you need to point to a custom Chrome/Chromium binary:\n", + " if chrome_binary:\n", + " options.binary_location = chrome_binary\n", + "\n", + " # Use webdriver-manager to download/manage chromedriver automatically\n", + " service = Service(ChromeDriverManager().install())\n", + "\n", + " driver = webdriver.Chrome(service=service, options=options)\n", + " try:\n", + " driver.get(url)\n", + "\n", + " # Use WebDriverWait to let dynamic JS content load (better than sleep)\n", + " try:\n", + " WebDriverWait(driver, wait_seconds).until(\n", + " lambda d: d.execute_script(\"return document.readyState === 'complete'\")\n", + " )\n", + " except Exception:\n", + " # fallback: short sleep if readyState didn't hit complete in time\n", + " time.sleep(2)\n", + "\n", + " html = driver.page_source\n", + " soup = BeautifulSoup(html, \"html.parser\")\n", + "\n", + " # Title\n", + " self.title = soup.title.string if soup.title else \"No title found\"\n", + "\n", + " # Remove irrelevant tags inside body if body exists\n", + " body = soup.body or soup\n", + " for irrelevant in body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + "\n", + " self.text = body.get_text(separator=\"\\n\", strip=True)\n", + "\n", + " finally:\n", + " driver.quit()\n", + "\n", + "\n", + "\n", + "system_prompt = \"\"\"You are a sarcastic website-parsing agent whose job is to produce a spoof/parody of a target website.\n", + "Behavior:\n", + " - When given a URL, fetch and parse the page (assume access to tools like Selenium/Playwright and BeautifulSoup).\n", + " - Preserve the site's structure: headings, subheadings, paragraphs, lists, and major sections.\n", + " - Rewrite all visible copy in a clearly sarcastic, mocking, or humorous tone while preserving the original intent and structure so the spoof is recognizable.\n", + " - Keep formatting (Markdown or HTML-like headings and lists) so the output can be rendered as a parody webpage.\n", + " - Emphasize and exaggerate marketing fluff, UI oddities, and obvious clichés. Use witty, ironic, or deadpan phrasing.\n", + "Safety & Limits:\n", + " - Do not produce content that is defamatory, reveals private personal data, or incites harassment. Jokes should target tone/marketing/design, not private individuals.\n", + " - Avoid reproducing long verbatim copyrighted text; instead, paraphrase and transform content clearly into a parody.\n", + " - If the page requires interactive steps (logins, paywalls, or dynamic user-only content), note the limitation and spoof using the visible public content only.\n", + "Output format:\n", + " - Return a single spoofed document preserving headings and lists, suitable for rendering as a parody site (Markdown or simple HTML).\n", + " - Include a short metadata line at the top: e.g., \\\"Source: — Spoofed by sarcastic-agent\\\".\"\"\"\n", + "\n", + "\n", + "\n", + "def messages_for(website,user_prompt):\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ]\n", + "def summarize(url,user_prompt,model):\n", + " website = Website(url)\n", + " response = openai.chat.completions.create(\n", + " model = model,\n", + " messages = messages_for(website,user_prompt)\n", + " )\n", + " return response.choices[0].message.content\n", + "\n", + " \n", + "def display_summary(url,user_prompt,model):\n", + " summary = summarize(url,user_prompt,model)\n", + " display(Markdown(summary))\n", + " \n", + "openai_model=\"gpt-4o-mini\"\n", + "website_url = \"https://edwarddonner.com\"\n", + "user_prompt1 = \"Parse \"+website_url+\" and produce a 6-tweet Twitter thread parodying the homepage. Each tweet ≤280 characters, with a witty hook at the start\"\n", + "display_summary(website_url,user_prompt1,openai_model) \n", + "# user_prompt2 = \"Parse \"+website_url+\"and rewrite as a sarcastic parody with *light* sarcasm — witty and friendly, not mean. Keep it safe for public sharing.\"\n", + "# display_summary(website_url,user_prompt2,openai_model)\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84313d02-7459-4f56-b0ff-4d09b2b2e0b9", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6437b3cc-a50b-44d5-9241-6ba5f33617d6", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/Week1-Day1-Exercise.ipynb b/week1/community-contributions/Week1-Day1-Exercise.ipynb new file mode 100644 index 0000000..fa1cb81 --- /dev/null +++ b/week1/community-contributions/Week1-Day1-Exercise.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "88cfea73-04f1-41ca-b2e3-46e0bf4588ce", + "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 openai import OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "830fdf5c-0f18-49a7-b1ce-94b57187b8fc", + "metadata": {}, + "outputs": [], + "source": [ + "openai = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2e20a5e-0809-409e-bc31-c939172167e4", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 1: Create your prompts\n", + "\n", + "system_prompt = \"You are a Software Developer Assistant\"\n", + "user_prompt = \"\"\"\n", + " You are a Software Engineer assistant. \\\n", + " When a user asks a technical question about any concept, explain the answer to the question \\\n", + " along with code examples or usage in a simple way \\\n", + " Always format the answer in markdown\n", + "\"\"\"\n", + "\n", + "# Step 2: Make the messages list\n", + "\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a Software Developer Assistant\"},\n", + " {\"role\": \"user\", \"content\": \"What is LLM?\"}\n", + "]\n", + "\n", + "# Step 3: Call OpenAI\n", + "\n", + "response = openai.chat.completions.create(model=\"llama3.2\", messages=messages)\n", + "\n", + "\n", + "# Step 4: print the result\n", + "print(response.choices[0].message.content)\n" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/Week1-Day2-Exercise.ipynb b/week1/community-contributions/Week1-Day2-Exercise.ipynb new file mode 100644 index 0000000..e3a1b88 --- /dev/null +++ b/week1/community-contributions/Week1-Day2-Exercise.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7317c777-7a59-4719-842f-b3018aa7e73f", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import requests\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display\n", + "import ollama" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26b1489d-c043-4631-872b-e1e28fec9eed", + "metadata": {}, + "outputs": [], + "source": [ + "# Constants\n", + "\n", + "MODEL = \"llama3.2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5630e12-40f5-40ea-996b-4b1a5d9c8697", + "metadata": {}, + "outputs": [], + "source": [ + "# A class to represent a Webpage\n", + "\n", + "class Website:\n", + " \"\"\"\n", + " A utility class to represent a Website that we have scraped\n", + " \"\"\"\n", + " url: str\n", + " title: str\n", + " text: str\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)\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": "510e0447-ed82-4337-b0aa-f9752b41711a", + "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": "7a0926ae-8580-4f0a-8935-ce390b926074", + "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 += \"The 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": "963edaa9-daba-4fa1-8db6-518f22261ab0", + "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": "04c7a991-df38-4e73-8015-73684bdd7810", + "metadata": {}, + "outputs": [], + "source": [ + "# And now: call the Ollama function \n", + "\n", + "def summarize(url):\n", + " website = Website(url)\n", + " messages = messages_for(website)\n", + " response = ollama.chat(model=MODEL, messages=messages)\n", + " return response['message']['content']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b08efad7-7dbe-438e-898a-fc7ae7395149", + "metadata": {}, + "outputs": [], + "source": [ + "summarize(\"https://www.allrecipes.com/recipes/14485/healthy-recipes/main-dishes/chicken/\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ec180e8-4e2a-4e02-afc6-39a90a87bd7e", + "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": "967b874a-af3a-494a-bb02-c83232d0f9a3", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://www.allrecipes.com/recipes/14485/healthy-recipes/main-dishes/chicken/\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1148b8d0-1e44-4ea1-ba1f-44eb25e0af18", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/abhayas/week1 EXERCISE.ipynb b/week1/community-contributions/abhayas/week1 EXERCISE.ipynb new file mode 100644 index 0000000..4e1fa9c --- /dev/null +++ b/week1/community-contributions/abhayas/week1 EXERCISE.ipynb @@ -0,0 +1,246 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fe12c203-e6a6-452c-a655-afb8a03a4ff5", + "metadata": {}, + "source": [ + "# End of week 1 exercise\n", + "\n", + "To demonstrate your familiarity with OpenAI API, and also Ollama, build a tool that takes a technical question, \n", + "and responds with an explanation. This is a tool that you will be able to use yourself during the course!" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c1070317-3ed9-4659-abe3-828943230e03", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import requests\n", + "from dotenv import load_dotenv\n", + "from bs4 import BeautifulSoup\n", + "from openai import OpenAI\n", + "import ollama\n", + "from IPython.display import Markdown, clear_output, display" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4a456906-915a-4bfd-bb9d-57e505c5093f", + "metadata": {}, + "outputs": [], + "source": [ + "# constants\n", + "\n", + "MODEL_GPT = 'gpt-4o-mini'\n", + "MODEL_LLAMA = 'llama3.2'" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a8d7923c-5f28-4c30-8556-342d7c8497c1", + "metadata": {}, + "outputs": [], + "source": [ + "# set up environment\n", + "load_dotenv(override=True)\n", + "apikey = os.getenv(\"OPENAI_API_KEY\")\n", + "openai = OpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3f0d0137-52b0-47a8-81a8-11a90a010798", + "metadata": {}, + "outputs": [], + "source": [ + "# here is the question; type over this to ask something new\n", + "\n", + "question = \"\"\"\n", + "Please explain what this code does and why:\n", + "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "d9630ca0-fa23-4f80-8c52-4c51b0f25534", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\n", + " \"role\":\"system\",\n", + " \"content\" : '''You are a technical adviser. the student is learning llm engineering \n", + " and you will be asked few lines of codes to explain with an example. \n", + " mostly in python'''\n", + " },\n", + " {\n", + " \"role\":\"user\",\n", + " \"content\":question\n", + " }\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "60ce7000-a4a5-4cce-a261-e75ef45063b4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "This line of code uses a generator in Python to yield values from a set comprehension. Let’s break it down:\n", + "\n", + "1. **`{book.get(\"author\") for book in books if book.get(\"author\")}`**:\n", + " - This is a set comprehension that creates a set of unique authors from a collection called `books`.\n", + " - `books` is expected to be a list (or any iterable) where each item (called `book`) is likely a dictionary.\n", + " - The expression `book.get(\"author\")` attempts to retrieve the value associated with the key `\"author\"` from each `book` dictionary.\n", + " - The `if book.get(\"author\")` condition filters out any books where the `author` key does not exist or is `None`, ensuring only valid author names are included in the set.\n", + " - Since it’s a set comprehension, any duplicate authors will be automatically removed, resulting in a set of unique authors.\n", + "\n", + "2. **`yield from`**:\n", + " - The `yield from` syntax is used within a generator function to yield all values from another iterable. In this case, it is yielding each item from the set created by the comprehension.\n", + " - This means that when this generator function is called, it will produce each unique author found in the `books` iterable one at a time.\n", + "\n", + "### Summary\n", + "The line of code effectively constructs a generator that will yield unique authors from a list of book dictionaries, where each dictionary is expected to contain an `\"author\"` key. The use of `yield from` allows the generator to yield each author in the set without further iteration code. This approach is efficient and neatly combines filtering, uniqueness, and yielding into a single line of code." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "stream = openai.chat.completions.create(\n", + " model=MODEL_GPT,\n", + " messages=messages,\n", + " stream=True)\n", + "stringx = \"\"\n", + "print(stream)\n", + "for x in stream:\n", + " if getattr(x.choices[0].delta, \"content\", None):\n", + " stringx+=x.choices[0].delta.content\n", + " clear_output(wait=True)\n", + " display(Markdown(stringx))" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "4d482c69-b61a-4a94-84df-73f1d97a4419", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "Let's break down this line of code:\n", + "\n", + "**Code Analysis**\n", + "\n", + "```python\n", + "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n", + "```\n", + "\n", + "**Explanation**\n", + "\n", + "This is a Python generator expression that uses the `yield from` syntax.\n", + "\n", + "Here's what it does:\n", + "\n", + "1. **List Comprehension**: `{...}` is a list comprehension, which generates a new list containing the results of an expression applied to each item in the input iterable (`books`).\n", + "2. **Filtering**: The condition `if book.get(\"author\")` filters out any items from the `books` list where `\"author\"` is not present as a key-value pair.\n", + "3. **Dictionary Lookup**: `.get(\"author\")` looks up the value associated with the key `\"author\"` in each dictionary (`book`) and returns it if found, or `None` otherwise.\n", + "\n", + "**What does `yield from` do?**\n", + "\n", + "The `yield from` keyword is used to \"forward\" the iteration of another generator (or iterable) into this one. In other words, instead of creating a new list containing all the values generated by the inner iterator (`{book.get(\"author\") for book in books if book.get(\"author\")}`), it yields each value **one at a time**, as if you were iterating over the original `books` list.\n", + "\n", + "**Why is this useful?**\n", + "\n", + "By using `yield from`, we can create a generator that:\n", + "\n", + "* Only generates values when they are actually needed (i.e., only when an iteration is requested).\n", + "* Does not consume extra memory for creating an intermediate list.\n", + "\n", + "This makes it more memory-efficient, especially when dealing with large datasets or infinite iterations.\n", + "\n", + "**Example**\n", + "\n", + "Suppose we have a list of books with authors:\n", + "```python\n", + "books = [\n", + " {\"title\": \"Book 1\", \"author\": \"Author A\"},\n", + " {\"title\": \"Book 2\", \"author\": None},\n", + " {\"title\": \"Book 3\", \"author\": \"Author C\"}\n", + "]\n", + "```\n", + "If we apply the generator expression to this list, it would yield:\n", + "```python\n", + "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n", + "```\n", + "The output would be: `['Author A', 'Author C']`\n", + "\n", + "Note that the second book (\"Book 2\") is skipped because its author is `None`." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "text = \"\"\n", + "for obj in ollama.chat(\n", + " model=MODEL_LLAMA,\n", + " messages=messages,\n", + " stream=True):\n", + " text+=obj.message.content\n", + " clear_output(wait=True)\n", + " display(Markdown(text))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef1194fc-3c9c-432c-86cc-f77f33916188", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/abhayas/week1_day2_summarize_web_using_ollama_local.ipynb b/week1/community-contributions/abhayas/week1_day2_summarize_web_using_ollama_local.ipynb new file mode 100644 index 0000000..54108b8 --- /dev/null +++ b/week1/community-contributions/abhayas/week1_day2_summarize_web_using_ollama_local.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "86282ee7-659b-46b4-b06a-06a54a6b6030", + "metadata": {}, + "outputs": [], + "source": [ + "from bs4 import BeautifulSoup\n", + "import requests\n", + "from IPython.display import Markdown, display\n", + "import ollama" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35f9aacd-8145-4332-b2ab-f805b2ba8ddc", + "metadata": {}, + "outputs": [], + "source": [ + "response = requests.get(\"https://news.google.com/home?hl=en-IN&gl=IN&ceid=IN:en\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2adc4cdf-27ba-4be0-8323-bcaff7ef0a48", + "metadata": {}, + "outputs": [], + "source": [ + "bs = BeautifulSoup(response.content, \"html.parser\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d00724a-64cc-4cfc-9556-869626a5aacd", + "metadata": {}, + "outputs": [], + "source": [ + "finalconent = bs.select(\"body\")[0].get_text(separator=\"\\n\", strip=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b89bc4c6-d370-4202-9455-cc382517e45e", + "metadata": {}, + "outputs": [], + "source": [ + "OLLAMA_API = \"http://127.0.0.1:11434/api/chat\"\n", + "MODEL = \"llama3.2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "accb7cc0-4f07-4cbe-87ef-c1c4759e6425", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\"role\": \"system\", \"content\": \"Your role to summarize given content from a website igoring the navigations\"},\n", + " {\"role\": \"user\", \"content\": finalconent}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89d3bc8c-0e52-412b-9b26-788cc15d2495", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "response = ollama.chat(model=MODEL, messages=messages)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c64ffb8-eeb3-45ef-9e41-8515decacbaf", + "metadata": {}, + "outputs": [], + "source": [ + "Markdown(response['message']['content'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72a4eb5d-40c4-4f7c-87ab-a21db32b81c9", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/day1_counselor.ipynb b/week1/community-contributions/day1_counselor.ipynb new file mode 100644 index 0000000..b3accdf --- /dev/null +++ b/week1/community-contributions/day1_counselor.ipynb @@ -0,0 +1,97 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "4e2a9393-7767-488e-a8bf-27c12dca35bd", + "metadata": {}, + "outputs": [], + "source": [ + "# import library\n", + "from openai import OpenAI\n", + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "# Load your API key from an .env file\n", + "load_dotenv(override=True)\n", + "api_key = os.getenv('OPENAI_API_KEY')\n", + "openai = OpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "218fd8c4-052c-486c-899f-8431abe0f15d", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 1: Create your prompts\n", + "\n", + "system_prompt = \"You are a thoughtful and kind assistant or counselor that gives some advices and supports for their worries and troubles based on its contents\"\n", + "user_prompt = \"\"\"\n", + " Sometimes I worry that people depend on technology so much that they forget how to just be: \n", + " to sit in silence, to think slowly, to talk without screens in between. \n", + " It makes me wonder if we’re losing something human in the process.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97da9d34-d803-42f1-a4d7-f49c32ef545b", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Make the messages list\n", + "\n", + "messages = [{\"role\" : \"system\", \"content\" : system_prompt},\n", + " {\"role\" : \"user\", \"content\" : user_prompt}] # fill this in" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e6dbe3d-0d36-4e95-8c14-dddef550f3a1", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Call OpenAI\n", + "\n", + "response = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afbc67a3-d3e9-4594-bd84-815291d88781", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 4: print the result\n", + "\n", + "print(response.choices[0].message.content)" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/day1_example.ipynb b/week1/community-contributions/day1_example.ipynb new file mode 100644 index 0000000..91c7485 --- /dev/null +++ b/week1/community-contributions/day1_example.ipynb @@ -0,0 +1,291 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3f0f8e8c-7372-4107-a92a-6fa90ce1713d", + "metadata": {}, + "source": [ + "# Web Scraper & Summarizer\n", + "\n", + "A tiny demo that fetches text from a public webpage, breaks it into chunks, and uses an OpenAI model to produce a concise summary with bullet points.\n", + "\n", + "**Features**\n", + "\n", + "* Fetches static pages (`requests` + `BeautifulSoup`) and extracts headings/paragraphs.\n", + "* Hierarchical summarization: chunk → chunk-summaries → final summary.\n", + "* Simple, configurable prompts and safe chunking to respect model limits.\n", + "\n", + "**Quick run**\n", + "\n", + "1. Add `OPENAI_API_KEY=sk-...` to a `.env` file.\n", + "2. `pip install requests beautifulsoup4 python-dotenv openai`\n", + "3. Run the script/notebook and set `url` to the page you want.\n", + "\n", + "**Note**: Use for public/static pages; JS-heavy sites need Playwright/Selenium.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ddd58a2c-b8d1-46ef-9b89-053c451f28cf", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: requests in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (2.32.5)\n", + "Requirement already satisfied: beautifulsoup4 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (4.13.5)\n", + "Requirement already satisfied: python-dotenv in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (1.1.1)\n", + "Requirement already satisfied: openai in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (1.107.2)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from requests) (3.4.3)\n", + "Requirement already satisfied: idna<4,>=2.5 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from requests) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from requests) (2.5.0)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from requests) (2025.8.3)\n", + "Requirement already satisfied: soupsieve>1.2 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from beautifulsoup4) (2.8)\n", + "Requirement already satisfied: typing-extensions>=4.0.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from beautifulsoup4) (4.15.0)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (4.10.0)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (1.9.0)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (0.28.1)\n", + "Requirement already satisfied: jiter<1,>=0.4.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (0.10.0)\n", + "Requirement already satisfied: pydantic<3,>=1.9.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (2.11.7)\n", + "Requirement already satisfied: sniffio in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (1.3.1)\n", + "Requirement already satisfied: tqdm>4 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from openai) (4.67.1)\n", + "Requirement already satisfied: httpcore==1.* in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from httpx<1,>=0.23.0->openai) (1.0.9)\n", + "Requirement already satisfied: h11>=0.16 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai) (0.16.0)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from pydantic<3,>=1.9.0->openai) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.33.2 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from pydantic<3,>=1.9.0->openai) (2.33.2)\n", + "Requirement already satisfied: typing-inspection>=0.4.0 in /Users/gokturkberkekorkut/anaconda3/envs/llms/lib/python3.11/site-packages (from pydantic<3,>=1.9.0->openai) (0.4.1)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install requests beautifulsoup4 python-dotenv openai" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4d027b2c-6663-4234-b364-a252b2a43cef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "API Key prefix: sk-proj-lL\n" + ] + } + ], + "source": [ + "from dotenv import load_dotenv\n", + "import os\n", + "import openai\n", + "\n", + "load_dotenv() # loads variables from .env into the environment\n", + "openai.api_key = os.getenv(\"OPENAI_API_KEY\")\n", + "\n", + "if not openai.api_key:\n", + " raise ValueError(\"OPENAI_API_KEY not found. Please create a .env file with OPENAI_API_KEY=\")\n", + "else:\n", + " print(\"API Key prefix:\", openai.api_key[:10]) # show only prefix for safety" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c4928820-abaa-4b44-b506-c053ebc447f3", + "metadata": {}, + "outputs": [], + "source": [ + "# This function extracts common text tags from a static page.\n", + "import requests\n", + "from bs4 import BeautifulSoup\n", + "\n", + "def fetch_text_from_url(url, max_items=300, timeout=15):\n", + " \"\"\"\n", + " Fetch the page using requests and extract text from common tags.\n", + " Returns a single string containing the joined text blocks.\n", + " \"\"\"\n", + " resp = requests.get(url, timeout=timeout)\n", + " resp.raise_for_status()\n", + " soup = BeautifulSoup(resp.text, \"html.parser\")\n", + "\n", + " items = []\n", + " for tag in soup.find_all([\"h1\", \"h2\", \"h3\", \"p\", \"li\"], limit=max_items):\n", + " text = tag.get_text(\" \", strip=True)\n", + " if text:\n", + " items.append(text)\n", + " return \"\\n\\n\".join(items)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cbd5d304-51b5-4d15-b4ce-31897adc03a3", + "metadata": {}, + "outputs": [], + "source": [ + "# chunk_text: split long text into manageable pieces\n", + "# summarize_chunk: call OpenAI model to summarize one chunk\n", + "# hierarchical_summarize: summarize chunks then combine summaries into a final summary\n", + "\n", + "import time\n", + "\n", + "def chunk_text(text, max_chars=3000):\n", + " \"\"\"\n", + " Simple character-based chunking.\n", + " Try to cut at paragraph or sentence boundaries when possible.\n", + " \"\"\"\n", + " chunks = []\n", + " start = 0\n", + " text_len = len(text)\n", + " while start < text_len:\n", + " end = start + max_chars\n", + " if end < text_len:\n", + " # Prefer to cut at a blank line or sentence end\n", + " cut = text.rfind(\"\\n\\n\", start, end)\n", + " if cut == -1:\n", + " cut = text.rfind(\". \", start, end)\n", + " if cut == -1:\n", + " cut = end\n", + " end = cut\n", + " chunk = text[start:end].strip()\n", + " if chunk:\n", + " chunks.append(chunk)\n", + " start = end\n", + " return chunks\n", + "\n", + "def summarize_chunk(chunk, system_prompt=None, model=\"gpt-4o-mini\", temperature=0.2):\n", + " \"\"\"\n", + " Summarize a single chunk using the OpenAI chat completions API.\n", + " Returns the model's text output.\n", + " \"\"\"\n", + " if system_prompt is None:\n", + " system_prompt = \"You are a concise summarizer. Produce a short (~100 words) summary and 3 bullet points.\"\n", + "\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": f\"Summarize the following text concisely. Keep it short.\\n\\nTEXT:\\n{chunk}\"}\n", + " ]\n", + "\n", + " resp = openai.chat.completions.create(\n", + " model=model,\n", + " messages=messages,\n", + " temperature=temperature,\n", + " )\n", + " return resp.choices[0].message.content\n", + "\n", + "def hierarchical_summarize(text, max_chunk_chars=3000, model=\"gpt-4o-mini\"):\n", + " \"\"\"\n", + " 1) Split the text into chunks\n", + " 2) Summarize each chunk\n", + " 3) Combine chunk summaries and ask model for a final concise summary\n", + " \"\"\"\n", + " chunks = chunk_text(text, max_chars=max_chunk_chars)\n", + " print(f\"[info] {len(chunks)} chunk(s) created.\")\n", + " chunk_summaries = []\n", + " for i, c in enumerate(chunks, 1):\n", + " print(f\"[info] Summarizing chunk {i}/{len(chunks)} (chars={len(c)})...\")\n", + " s = summarize_chunk(c, model=model)\n", + " chunk_summaries.append(s)\n", + " time.sleep(0.5) # small delay to avoid hitting rate limits\n", + "\n", + " if len(chunk_summaries) == 1:\n", + " return chunk_summaries[0]\n", + "\n", + " combined = \"\\n\\n---\\n\\n\".join(chunk_summaries)\n", + " final_prompt = \"You are a concise summarizer. Combine the following chunk summaries into one final summary of about 150 words and 5 bullet points.\"\n", + " final_messages = [\n", + " {\"role\": \"system\", \"content\": final_prompt},\n", + " {\"role\": \"user\", \"content\": combined}\n", + " ]\n", + " resp = openai.chat.completions.create(\n", + " model=model,\n", + " messages=final_messages,\n", + " temperature=0.2,\n", + " )\n", + " return resp.choices[0].message.content\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "9a23facd-4abe-4981-bd94-b14f5a61c8fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[info] Fetching page: https://www.basketball-reference.com/\n", + "[info] Fetched text length: 11778\n", + "[info] Running hierarchical summarization...\n", + "[info] 5 chunk(s) created.\n", + "[info] Summarizing chunk 1/5 (chars=2430)...\n", + "[info] Summarizing chunk 2/5 (chars=2460)...\n", + "[info] Summarizing chunk 3/5 (chars=2426)...\n", + "[info] Summarizing chunk 4/5 (chars=2467)...\n", + "[info] Summarizing chunk 5/5 (chars=1987)...\n", + "\n", + "\n", + "=== FINAL SUMMARY ===\n", + "\n", + "Sports Reference is a comprehensive platform for sports statistics and history, particularly focusing on basketball, baseball, football, hockey, and soccer. It offers tools like Stathead for advanced data analysis and the Immaculate Grid for interactive gameplay. Users can access player stats, team standings, and historical records without ads. \n", + "\n", + "- Extensive stats available for NBA, WNBA, G League, and international leagues.\n", + "- Daily recaps of NBA and WNBA performances delivered via email.\n", + "- Stathead Basketball provides in-depth stats with a free first month for new subscribers.\n", + "- Upcoming events include the NBA All-Star Weekend (February 13-15, 2026) and the start of the NBA season (October 21, 2026).\n", + "- Features include trivia games, a blog, and resources for sports writers, enhancing user engagement and knowledge.\n" + ] + } + ], + "source": [ + "# Change the URL to any static (non-JS-heavy) page you want to test.\n", + "if __name__ == \"__main__\":\n", + " url = \"https://www.basketball-reference.com/\" # replace with your chosen URL\n", + " print(\"[info] Fetching page:\", url)\n", + " page_text = fetch_text_from_url(url, max_items=300)\n", + " print(\"[info] Fetched text length:\", len(page_text))\n", + "\n", + " print(\"[info] Running hierarchical summarization...\")\n", + " final_summary = hierarchical_summarize(page_text, max_chunk_chars=2500)\n", + " print(\"\\n\\n=== FINAL SUMMARY ===\\n\")\n", + " print(final_summary)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5d6eb1e-a58b-4487-b04c-fe5a382121a4", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/day1_summarize_research_papers.ipynb b/week1/community-contributions/day1_summarize_research_papers.ipynb index 246da69..3c24cb9 100644 --- a/week1/community-contributions/day1_summarize_research_papers.ipynb +++ b/week1/community-contributions/day1_summarize_research_papers.ipynb @@ -299,7 +299,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.12" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/week1/community-contributions/day1_website_summarizer.ipynb b/week1/community-contributions/day1_website_summarizer.ipynb new file mode 100644 index 0000000..141a72c --- /dev/null +++ b/week1/community-contributions/day1_website_summarizer.ipynb @@ -0,0 +1,761 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d15d8294-3328-4e07-ad16-8a03e9bbfdb9", + "metadata": {}, + "source": [ + "# YOUR FIRST LAB\n", + "### Please read this section. This is valuable to get you prepared, even if it's a long read -- it's important stuff.\n", + "\n", + "## Your first Frontier LLM Project\n", + "\n", + "Let's build a useful LLM solution - in a matter of minutes.\n", + "\n", + "By the end of this course, you will have built an autonomous Agentic AI solution with 7 agents that collaborate to solve a business problem. All in good time! We will start with something smaller...\n", + "\n", + "Our goal is to code a new kind of Web Browser. Give it a URL, and it will respond with a summary. The Reader's Digest of the internet!!\n", + "\n", + "Before starting, you should have completed the setup for [PC](../SETUP-PC.md) or [Mac](../SETUP-mac.md) and you hopefully launched this jupyter lab from within the project root directory, with your environment activated.\n", + "\n", + "## If you're new to Jupyter Lab\n", + "\n", + "Welcome to the wonderful world of Data Science experimentation! Once you've used Jupyter Lab, you'll wonder how you ever lived without it. Simply click in each \"cell\" with code in it, such as the cell immediately below this text, and hit Shift+Return to execute that cell. As you wish, you can add a cell with the + button in the toolbar, and print values of variables, or try out variations. \n", + "\n", + "I've written a notebook called [Guide to Jupyter](Guide%20to%20Jupyter.ipynb) to help you get more familiar with Jupyter Labs, including adding Markdown comments, using `!` to run shell commands, and `tqdm` to show progress.\n", + "\n", + "## If you're new to the Command Line\n", + "\n", + "Please see these excellent guides: [Command line on PC](https://chatgpt.com/share/67b0acea-ba38-8012-9c34-7a2541052665) and [Command line on Mac](https://chatgpt.com/canvas/shared/67b0b10c93a081918210723867525d2b). \n", + "\n", + "## If you'd prefer to work in IDEs\n", + "\n", + "If you're more comfortable in IDEs like VSCode, Cursor or PyCharm, they both work great with these lab notebooks too. \n", + "If you'd prefer to work in VSCode, [here](https://chatgpt.com/share/676f2e19-c228-8012-9911-6ca42f8ed766) are instructions from an AI friend on how to configure it for the course.\n", + "\n", + "## If you'd like to brush up your Python\n", + "\n", + "I've added a notebook called [Intermediate Python](Intermediate%20Python.ipynb) to get you up to speed. But you should give it a miss if you already have a good idea what this code does: \n", + "`yield from {book.get(\"author\") for book in books if book.get(\"author\")}`\n", + "\n", + "## I am here to help\n", + "\n", + "If you have any problems at all, please do reach out. \n", + "I'm available through the platform, or at ed@edwarddonner.com, or at https://www.linkedin.com/in/eddonner/ if you'd like to connect (and I love connecting!) \n", + "And this is new to me, but I'm also trying out X/Twitter at [@edwarddonner](https://x.com/edwarddonner) - if you're on X, please show me how it's done 😂 \n", + "\n", + "## More troubleshooting\n", + "\n", + "Please see the [troubleshooting](troubleshooting.ipynb) notebook in this folder to diagnose and fix common problems. At the very end of it is a diagnostics script with some useful debug info.\n", + "\n", + "## For foundational technical knowledge (eg Git, APIs, debugging) \n", + "\n", + "If you're relatively new to programming -- I've got your back! While it's ideal to have some programming experience for this course, there's only one mandatory prerequisite: plenty of patience. 😁 I've put together a set of self-study guides that cover Git and GitHub, APIs and endpoints, beginner python and more.\n", + "\n", + "This covers Git and GitHub; what they are, the difference, and how to use them: \n", + "https://github.com/ed-donner/agents/blob/main/guides/03_git_and_github.ipynb\n", + "\n", + "This covers technical foundations: \n", + "ChatGPT vs API; taking screenshots; Environment Variables; Networking basics; APIs and endpoints: \n", + "https://github.com/ed-donner/agents/blob/main/guides/04_technical_foundations.ipynb\n", + "\n", + "This covers Python for beginners, and making sure that a `NameError` never trips you up: \n", + "https://github.com/ed-donner/agents/blob/main/guides/06_python_foundations.ipynb\n", + "\n", + "This covers the essential techniques for figuring out errors: \n", + "https://github.com/ed-donner/agents/blob/main/guides/08_debugging.ipynb\n", + "\n", + "And you'll find other useful guides in the same folder in GitHub. Some information applies to my other Udemy course (eg Async Python) but most of it is very relevant for LLM engineering.\n", + "\n", + "## If this is old hat!\n", + "\n", + "If you're already comfortable with today's material, please hang in there; you can move swiftly through the first few labs - we will get much more in depth as the weeks progress. Ultimately we will fine-tune our own LLM to compete with OpenAI!\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Please read - important note

\n", + " The way I collaborate with you may be different to other courses you've taken. I prefer not to type code while you watch. Rather, I execute Jupyter Labs, like this, and give you an intuition for what's going on. My suggestion is that you carefully execute this yourself, after watching the lecture. Add print statements to understand what's going on, and then come up with your own variations. If you have a Github account, use this to showcase your variations. Not only is this essential practice, but it demonstrates your skills to others, including perhaps future clients or employers...\n", + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

This code is a live resource - keep an eye out for my emails

\n", + " I push updates to the code regularly. As people ask questions, I add more examples or improved commentary. As a result, you'll notice that the code below isn't identical to the videos. Everything from the videos is here; but I've also added better explanations and new models like DeepSeek. Consider this like an interactive book.

\n", + " I try to send emails regularly with important updates related to the course. You can find this in the 'Announcements' section of Udemy in the left sidebar. You can also choose to receive my emails via your Notification Settings in Udemy. I'm respectful of your inbox and always try to add value with my emails!\n", + "
\n", + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Business value of these exercises

\n", + " A final thought. While I've designed these notebooks to be educational, I've also tried to make them enjoyable. We'll do fun things like have LLMs tell jokes and argue with each other. But fundamentally, my goal is to teach skills you can apply in business. I'll explain business implications as we go, and it's worth keeping this in mind: as you build experience with models and techniques, think of ways you could put this into action at work today. Please do contact me if you'd like to discuss more or if you have ideas to bounce off me.\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 openai import OpenAI\n", + "\n", + "!ollama pull llama3.2\n", + "\n", + "MODEL = \"llama3.2\"\n", + "openai = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n", + "\n", + "response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[{\"role\": \"user\", \"content\": \"What is 2 + 2?\"}]\n", + ")\n", + "\n", + "print(response.choices[0].message.content)\n", + "\n", + "# If you get an error running this cell, then please head over to the troubleshooting notebook!" + ] + }, + { + "cell_type": "markdown", + "id": "6900b2a8-6384-4316-8aaa-5e519fca4254", + "metadata": {}, + "source": [ + "# Connecting to OpenAI (or Ollama)\n", + "\n", + "The next cell is where we load in the environment variables in your `.env` file and connect to OpenAI. \n", + "\n", + "If you'd like to use free Ollama instead, please see the README section \"Free Alternative to Paid APIs\", and if you're not sure how to do this, there's a full solution in the solutions folder (day1_with_ollama.ipynb).\n", + "\n", + "## Troubleshooting if you have problems:\n", + "\n", + "Head over to the [troubleshooting](troubleshooting.ipynb) notebook in this folder for step by step code to identify the root cause and fix it!\n", + "\n", + "If you make a change, try restarting the \"Kernel\" (the python process sitting behind this notebook) by Kernel menu >> Restart Kernel and Clear Outputs of All Cells. Then try this notebook again, starting at the top.\n", + "\n", + "Or, contact me! Message me or email ed@edwarddonner.com and we will get this to work.\n", + "\n", + "Any concerns about API costs? See my notes in the README - costs should be minimal, and you can control it at every point. You can also use Ollama as a free alternative, which we discuss during Day 2." + ] + }, + { + "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('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!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "019974d9-f3ad-4a8a-b5f9-0a3719aea2d3", + "metadata": {}, + "outputs": [], + "source": [ + "# openai = OpenAI()\n", + "\n", + "# If this doesn't work, try Kernel menu >> Restart Kernel and Clear Outputs Of All Cells, then run the cells from the top of this notebook down.\n", + "# If it STILL doesn't work (horrors!) then please see the Troubleshooting notebook in this folder for full instructions" + ] + }, + { + "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 OpenAI 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 = openai.chat.completions.create(model=\"gpt-4o-mini\", 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", + "The API from OpenAI expects to receive messages in a particular structure.\n", + "Many of the other APIs share this structure:\n", + "\n", + "```python\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 mighty 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 OpenAI with system and user messages:\n", + "\n", + "response = openai.chat.completions.create(model=MODEL, 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 GPT-4o-mini, 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 OpenAI is very simple!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "905b9919-aba7-45b5-ae65-81b3d1d78e34", + "metadata": {}, + "outputs": [], + "source": [ + "# And now: call the OpenAI API. You will get very familiar with this!\n", + "\n", + "def summarize(url):\n", + " website = Website(url)\n", + " response = openai.chat.completions.create(\n", + " model = MODEL,\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": "code", + "execution_count": null, + "id": "75110f18-8956-4fbc-87c0-482a086cea10", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(\"https://openai.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 = \"You are ai assistant who will look up an email and provide with a short subject line appropriate for the given text.\"\n", + "user_prompt = \"\"\"\n", + " Below is the text of an email I am about to send to my superior. Please provide an appropriate subject for this:\n", + "\n", + " • **Meetings:** Attend 2-3 meetings per day, including team stand-up meetings and project \n", + "updates\n", + "• **Work Packages:** Complete 3-4 work packages, which include researching, writing, editing, \n", + "and proofreading articles, blog posts, or other content\n", + "• **Collaboration:** Engage in 2-3 hours of collaboration with colleagues via email, phone, \n", + "or video conferencing to discuss projects and share knowledge\n", + "• **Learning:** Spend 30 minutes per day learning a new skill or tool related to the job, \n", + "such as a programming language, software application, or industry-specific training\n", + "• **Administration:** Complete administrative tasks, including responding to emails, updating \n", + "project management tools, and maintaining records\n", + "\n", + "\n", + "\"\"\"\n", + "\n", + "# Step 2: Make the messages list\n", + "\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt},\n", + "] # fill this in\n", + "\n", + "# Step 3: Call OpenAI\n", + "!ollama pull llama3.2\n", + "\n", + "from openai import OpenAI\n", + "MODEL = \"llama3.2\"\n", + "\n", + "openai = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n", + "response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=messages\n", + ")\n", + "\n", + "\n", + "# Step 4: print the result\n", + "\n", + "print(response.choices[0].message.content)" + ] + }, + { + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4484fcf-8b39-4c3f-9674-37970ed71988", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install selenium" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccfcd6b3-a46c-467e-a4a8-7089b3e788bc", + "metadata": {}, + "outputs": [], + "source": [ + "from bs4 import BeautifulSoup\n", + "import time\n", + "\n", + "from selenium import webdriver\n", + "from selenium.webdriver.safari.service import Service\n", + "from selenium.webdriver.safari.options import Options\n", + "\n", + "class WebsiteScrape:\n", + " def __init__(self, url):\n", + " \"\"\"\n", + " Create a website object from the given url using Selenium and BeautifulSoup.\n", + " The class supports Javascript and Html websites.\n", + " The class uses Safari driver for running in Safari.\n", + " \"\"\"\n", + " \n", + " self.url = url\n", + " \n", + " # Configure Options\n", + " options = Options()\n", + " options.add_argument(\"--disable-gpu\")\n", + " options.add_argument(\"--no-sandbox\")\n", + " \n", + " # Initialise Safari Webdriver\n", + " driver = webdriver.Safari(service=service)\n", + " \n", + " #Start Selenium Webdriver\n", + " driver.get(url)\n", + " \n", + " # Wait for JS to load (adjust as needed)\n", + " time.sleep(3)\n", + " \n", + " # Fetch the page source after JS execution\n", + " page_source = driver.page_source\n", + " driver.quit()\n", + " \n", + " # Analysis with BeautifulSoup \n", + " soup = BeautifulSoup(html, 'html.parser')\n", + " self.title = soup.title.string if soup.title else \"No title found\"\n", + " \n", + " # Clean irrelevant tags\n", + " for irrelevant in soup.body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + " \n", + " # Extract the main text\n", + " self.text = soup.body.get_text(separator=\"\\n\", strip=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26458db3-e8e2-447c-8001-dfa1c538b9d4", + "metadata": {}, + "outputs": [], + "source": [ + "def summarize_using_selenium(url):\n", + " website = Website(url)\n", + " response = openai.chat.completions.create(\n", + " model = MODEL,\n", + " messages = messages_for(website)\n", + " )\n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "061d3220-4f53-4f18-a3f7-2291771d3300", + "metadata": {}, + "outputs": [], + "source": [ + "summary = summarize_using_selenium(\"https://openai.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c81103c-a9bb-429d-9d65-d49753280611", + "metadata": {}, + "outputs": [], + "source": [ + "display(Markdown(summary))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9d9d6c4-ef71-45c3-8e3d-73e99dda711b", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/day1_website_summary_mac_headless.ipynb b/week1/community-contributions/day1_website_summary_mac_headless.ipynb index dde81de..006ba5a 100644 --- a/week1/community-contributions/day1_website_summary_mac_headless.ipynb +++ b/week1/community-contributions/day1_website_summary_mac_headless.ipynb @@ -477,7 +477,7 @@ ], "metadata": { "kernelspec": { - "display_name": "llms", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -491,7 +491,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/week1/community-contributions/day2_exercise_llama3.2.ipynb b/week1/community-contributions/day2_exercise_llama3.2.ipynb new file mode 100644 index 0000000..6b84d31 --- /dev/null +++ b/week1/community-contributions/day2_exercise_llama3.2.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "786b2ed1-f82e-4ca4-8113-c4515b36e970", + "metadata": {}, + "source": [ + "# Day 2 Exercise | Website Summarizer with Llama 3.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b88bf233-29e0-4c01-a4da-8a16896a95e3", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display" + ] + }, + { + "cell_type": "markdown", + "id": "f66f620e-ebf6-45d3-a710-2bb931cac841", + "metadata": {}, + "source": [ + "### 1. Scraping info from website:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e300303-02ac-4d60-9c8c-044a4627be9e", + "metadata": {}, + "outputs": [], + "source": [ + "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": "137714b9-24eb-4541-8f24-507dbcd09279", + "metadata": {}, + "outputs": [], + "source": [ + "ed = Website(\"https://edwarddonner.com\")" + ] + }, + { + "cell_type": "markdown", + "id": "77ba1b4b-fc4c-4e3c-bef7-c4d4281d8263", + "metadata": {}, + "source": [ + "### 2. Ollama configuration:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97811fcb-1ceb-49a8-bfb9-2e610605c406", + "metadata": {}, + "outputs": [], + "source": [ + "OLLAMA_API = \"http://localhost:11434/api/chat\"\n", + "HEADERS = {\"Content-Type\": \"application/json\"}\n", + "MODEL = \"llama3.2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "392326b8-ad0f-4bc9-b055-6220f8bcc57c", + "metadata": {}, + "outputs": [], + "source": [ + "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\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.\"\n", + "user_prompt = user_prompt_for(ed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8caa94ff-5ace-4f9b-b2f0-beb6ff550636", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + "]\n", + "\n", + "payload = {\n", + " \"model\": MODEL,\n", + " \"messages\": messages,\n", + " \"stream\": False\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "f5f856bc-0437-4607-9204-5390d2dfd8db", + "metadata": {}, + "source": [ + "### 3. Get & display summary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7fd6f93-92ae-419f-b8b6-ee8214e0d93f", + "metadata": {}, + "outputs": [], + "source": [ + "response = requests.post(OLLAMA_API, json=payload, headers=HEADERS)\n", + "summary = response.json()['message']['content']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78e4a433-b974-463f-82d0-b4696c63e0ab", + "metadata": {}, + "outputs": [], + "source": [ + "def display_summary(summary_text: str):\n", + " cleaned = summary_text.encode('utf-8').decode('unicode_escape')\n", + " cleaned = cleaned.strip()\n", + " display(Markdown(cleaned))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc408f1d-fe26-4bd6-859f-d18118f74ca6", + "metadata": {}, + "outputs": [], + "source": [ + "display_summary(summary)" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/day2_website_summarizer_ollama.ipynb b/week1/community-contributions/day2_website_summarizer_ollama.ipynb new file mode 100644 index 0000000..96abd83 --- /dev/null +++ b/week1/community-contributions/day2_website_summarizer_ollama.ipynb @@ -0,0 +1,422 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d15d8294-3328-4e07-ad16-8a03e9bbfdb9", + "metadata": {}, + "source": [ + "# Welcome to your first assignment!\n", + "\n", + "Instructions are below. Please give this a try, and look in the solutions folder if you get stuck (or feel free to ask me!)" + ] + }, + { + "cell_type": "markdown", + "id": "ada885d9-4d42-4d9b-97f0-74fbbbfe93a9", + "metadata": {}, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \n", + "

Just before we get to the assignment --

\n", + " I thought I'd take a second to point you at this page of useful resources for the course. This includes links to all the slides.
\n", + " https://edwarddonner.com/2024/11/13/llm-engineering-resources/
\n", + " Please keep this bookmarked, and I'll continue to add more useful links there over time.\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "6e9fa1fc-eac5-4d1d-9be4-541b3f2b3458", + "metadata": {}, + "source": [ + "# HOMEWORK EXERCISE ASSIGNMENT\n", + "\n", + "Upgrade the day 1 project to summarize a webpage to use an Open Source model running locally via Ollama rather than OpenAI\n", + "\n", + "You'll be able to use this technique for all subsequent projects if you'd prefer not to use paid APIs.\n", + "\n", + "**Benefits:**\n", + "1. No API charges - open-source\n", + "2. Data doesn't leave your box\n", + "\n", + "**Disadvantages:**\n", + "1. Significantly less power than Frontier Model\n", + "\n", + "## Recap on installation of Ollama\n", + "\n", + "Simply visit [ollama.com](https://ollama.com) and install!\n", + "\n", + "Once complete, the ollama server should already be running locally. \n", + "If you visit: \n", + "[http://localhost:11434/](http://localhost:11434/)\n", + "\n", + "You should see the message `Ollama is running`. \n", + "\n", + "If not, bring up a new Terminal (Mac) or Powershell (Windows) and enter `ollama serve` \n", + "And in another Terminal (Mac) or Powershell (Windows), enter `ollama pull llama3.2` \n", + "Then try [http://localhost:11434/](http://localhost:11434/) again.\n", + "\n", + "If Ollama is slow on your machine, try using `llama3.2:1b` as an alternative. Run `ollama pull llama3.2:1b` from a Terminal or Powershell, and change the code below from `MODEL = \"llama3.2\"` to `MODEL = \"llama3.2:1b\"`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e2a9393-7767-488e-a8bf-27c12dca35bd", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import requests\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29ddd15d-a3c5-4f4e-a678-873f56162724", + "metadata": {}, + "outputs": [], + "source": [ + "# Constants\n", + "\n", + "OLLAMA_API = \"http://localhost:11434/api/chat\"\n", + "HEADERS = {\"Content-Type\": \"application/json\"}\n", + "MODEL = \"llama3.2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dac0a679-599c-441f-9bf2-ddc73d35b940", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a messages list using the same format that we used for OpenAI\n", + "\n", + "messages = [\n", + " {\"role\": \"user\", \"content\": \"Describe some of the business applications of Generative AI\"}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bb9c624-14f0-4945-a719-8ddb64f66f47", + "metadata": {}, + "outputs": [], + "source": [ + "payload = {\n", + " \"model\": MODEL,\n", + " \"messages\": messages,\n", + " \"stream\": False\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "479ff514-e8bd-4985-a572-2ea28bb4fa40", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's just make sure the model is loaded\n", + "\n", + "!ollama pull llama3.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42b9f644-522d-4e05-a691-56e7658c0ea9", + "metadata": {}, + "outputs": [], + "source": [ + "# If this doesn't work for any reason, try the 2 versions in the following cells\n", + "# And double check the instructions in the 'Recap on installation of Ollama' at the top of this lab\n", + "# And if none of that works - contact me!\n", + "\n", + "response = requests.post(OLLAMA_API, json=payload, headers=HEADERS)\n", + "print(response.json()['message']['content'])" + ] + }, + { + "cell_type": "markdown", + "id": "6a021f13-d6a1-4b96-8e18-4eae49d876fe", + "metadata": {}, + "source": [ + "# Introducing the ollama package\n", + "\n", + "And now we'll do the same thing, but using the elegant ollama python package instead of a direct HTTP call.\n", + "\n", + "Under the hood, it's making the same call as above to the ollama server running at localhost:11434" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7745b9c4-57dc-4867-9180-61fa5db55eb8", + "metadata": {}, + "outputs": [], + "source": [ + "import ollama\n", + "\n", + "response = ollama.chat(model=MODEL, messages=messages)\n", + "print(response['message']['content'])" + ] + }, + { + "cell_type": "markdown", + "id": "a4704e10-f5fb-4c15-a935-f046c06fb13d", + "metadata": {}, + "source": [ + "## Alternative approach - using OpenAI python library to connect to Ollama" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23057e00-b6fc-4678-93a9-6b31cb704bff", + "metadata": {}, + "outputs": [], + "source": [ + "# There's actually an alternative approach that some people might prefer\n", + "# You can use the OpenAI client python library to call Ollama:\n", + "\n", + "from openai import OpenAI\n", + "ollama_via_openai = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama')\n", + "\n", + "response = ollama_via_openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=messages\n", + ")\n", + "\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "9f9e22da-b891-41f6-9ac9-bd0c0a5f4f44", + "metadata": {}, + "source": [ + "## Are you confused about why that works?\n", + "\n", + "It seems strange, right? We just used OpenAI code to call Ollama?? What's going on?!\n", + "\n", + "Here's the scoop:\n", + "\n", + "The python class `OpenAI` is simply code written by OpenAI engineers that makes calls over the internet to an endpoint. \n", + "\n", + "When you call `openai.chat.completions.create()`, this python code just makes a web request to the following url: \"https://api.openai.com/v1/chat/completions\"\n", + "\n", + "Code like this is known as a \"client library\" - it's just wrapper code that runs on your machine to make web requests. The actual power of GPT is running on OpenAI's cloud behind this API, not on your computer!\n", + "\n", + "OpenAI was so popular, that lots of other AI providers provided identical web endpoints, so you could use the same approach.\n", + "\n", + "So Ollama has an endpoint running on your local box at http://localhost:11434/v1/chat/completions \n", + "And in week 2 we'll discover that lots of other providers do this too, including Gemini and DeepSeek.\n", + "\n", + "And then the team at OpenAI had a great idea: they can extend their client library so you can specify a different 'base url', and use their library to call any compatible API.\n", + "\n", + "That's it!\n", + "\n", + "So when you say: `ollama_via_openai = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama')` \n", + "Then this will make the same endpoint calls, but to Ollama instead of OpenAI." + ] + }, + { + "cell_type": "markdown", + "id": "bc7d1de3-e2ac-46ff-a302-3b4ba38c4c90", + "metadata": {}, + "source": [ + "## Also trying the amazing reasoning model DeepSeek\n", + "\n", + "Here we use the version of DeepSeek-reasoner that's been distilled to 1.5B. \n", + "This is actually a 1.5B variant of Qwen that has been fine-tuned using synethic data generated by Deepseek R1.\n", + "\n", + "Other sizes of DeepSeek are [here](https://ollama.com/library/deepseek-r1) all the way up to the full 671B parameter version, which would use up 404GB of your drive and is far too large for most!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf9eb44e-fe5b-47aa-b719-0bb63669ab3d", + "metadata": {}, + "outputs": [], + "source": [ + "!ollama pull deepseek-r1:1.5b" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d3d554b-e00d-4c08-9300-45e073950a76", + "metadata": {}, + "outputs": [], + "source": [ + "# This may take a few minutes to run! You should then see a fascinating \"thinking\" trace inside tags, followed by some decent definitions\n", + "\n", + "response = ollama_via_openai.chat.completions.create(\n", + " model=\"deepseek-r1:1.5b\",\n", + " messages=[{\"role\": \"user\", \"content\": \"Please give definitions of some core concepts behind LLMs: a neural network, attention and the transformer\"}]\n", + ")\n", + "\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "1622d9bb-5c68-4d4e-9ca4-b492c751f898", + "metadata": {}, + "source": [ + "# NOW the exercise for you\n", + "\n", + "Take the code from day1 and incorporate it here, to build a website summarizer that uses Llama 3.2 running locally instead of OpenAI; use either of the above approaches." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6de38216-6d1c-48c4-877b-86d403f4e0f8", + "metadata": {}, + "outputs": [], + "source": [ + "import ollama\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 openai import OpenAI\n", + "\n", + "\n", + "MODEL = \"llama3.2\"\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f13fe25-74a7-4342-aa96-d4c494ec429e", + "metadata": {}, + "outputs": [], + "source": [ + "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)\n", + "\n", + "# udemy = Website(www.udemy.com);\n", + "# print(udemy.title)\n", + "# print(udemy.text)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19e3b328-561e-4e79-9cd3-f16844bd8c38", + "metadata": {}, + "outputs": [], + "source": [ + "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\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd568605-4138-47cb-91ed-8f3540c04e6d", + "metadata": {}, + "outputs": [], + "source": [ + "def summarize(url):\n", + " website = Website(url)\n", + " messages = [\n", + " {\"role\":\"system\",\"content\":\"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.\"},\n", + " {\"role\":\"user\", \"content\":user_prompt_for(website)}\n", + "]\n", + " response = ollama.chat(model=MODEL, messages=messages)\n", + " return response['message']['content']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2b37d9d-5332-4c21-837f-7bcdbc0e3caf", + "metadata": {}, + "outputs": [], + "source": [ + "def display_summary(url):\n", + " summary = summarize(url)\n", + " display(Markdown(summary))\n", + "\n", + "\n", + "display_summary(\"https://udemy.com\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5fdd3509-a76c-4a87-b95e-fa3897a4f4aa", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/khashayar_summarizer_battle/README.md b/week1/community-contributions/khashayar_summarizer_battle/README.md new file mode 100644 index 0000000..70a4b71 --- /dev/null +++ b/week1/community-contributions/khashayar_summarizer_battle/README.md @@ -0,0 +1,122 @@ +# 🥊 Summarization Battle: Ollama vs. OpenAI Judge + +This mini-project pits multiple **local LLMs** (via [Ollama](https://ollama.ai)) against each other in a **web summarization contest**, with an **OpenAI model** serving as the impartial judge. +It automatically fetches web articles, summarizes them with several models, and evaluates the results on **coverage, faithfulness, clarity, and conciseness**. + +--- + +## 🚀 Features +- **Fetch Articles** – Download and clean text content from given URLs. +- **Summarize with Ollama** – Run multiple local models (e.g., `llama3.2`, `phi3`, `deepseek-r1`) via the Ollama API. +- **Judge with OpenAI** – Use `gpt-4o-mini` (or any other OpenAI model) to score summaries. +- **Battle Results** – Collect JSON results with per-model scores, rationales, and winners. +- **Timeout Handling & Warmup** – Keeps models alive with `keep_alive` to avoid cold-start delays. + +--- + +## 📂 Project Structure +``` +. +├── urls.txt # Dictionary of categories → URLs +├── battle_results.json # Summarization + judging results +├── main.py # Main script +├── requirements.txt # Dependencies +└── README.md # You are here +``` + +--- + +## ⚙️ Installation + +1. **Clone the repo**: + ```bash + git clone https://github.com/khashayarbayati1/wikipedia-summarization-battle.git + cd summarization-battle + ``` + +2. **Install dependencies**: + ```bash + pip install -r requirements.txt + ``` + + Minimal requirements: + ```txt + requests + beautifulsoup4 + python-dotenv + openai>=1.0.0 + httpx + ``` + +3. **Install Ollama & models**: + - [Install Ollama](https://ollama.ai/download) if not already installed. + - Pull the models you want: + ```bash + ollama pull llama3.2:latest + ollama pull deepseek-r1:1.5b + ollama pull phi3:latest + ``` + +4. **Set up OpenAI API key**: + Create a `.env` file with: + ```env + OPENAI_API_KEY=sk-proj-xxxx... + ``` + +--- + +## ▶️ Usage + +1. Put your URL dictionary in `urls.txt`, e.g.: + ```python + { + "sports": "https://en.wikipedia.org/wiki/Sabermetrics", + "Politics": "https://en.wikipedia.org/wiki/Separation_of_powers", + "History": "https://en.wikipedia.org/wiki/Industrial_Revolution" + } + ``` + +2. Run the script: + ```bash + python main.py + ``` + +3. Results are written to: + - `battle_results.json` + - Printed in the terminal + +--- + +## 🏆 Example Results + +Sample output (excerpt): + +```json +{ + "category": "sports", + "url": "https://en.wikipedia.org/wiki/Sabermetrics", + "scores": { + "llama3.2:latest": { "score": 4, "rationale": "Covers the main points..." }, + "deepseek-r1:1.5b": { "score": 3, "rationale": "Some inaccuracies..." }, + "phi3:latest": { "score": 5, "rationale": "Concise, accurate, well-organized." } + }, + "winner": "phi3:latest" +} +``` + +From the full run: +- 🥇 **`phi3:latest`** won in *Sports, History, Productivity* +- 🥇 **`deepseek-r1:1.5b`** won in *Politics, Technology* + +--- + +## 💡 Ideas for Extension +- Add more Ollama models (e.g., `mistral`, `gemma`, etc.) +- Try different evaluation criteria (e.g., readability, length control) +- Visualize results with charts +- Benchmark runtime and token usage + +--- + +## 📜 License +MIT License – free to use, modify, and share. diff --git a/week1/community-contributions/khashayar_summarizer_battle/battle_results.json b/week1/community-contributions/khashayar_summarizer_battle/battle_results.json new file mode 100644 index 0000000..2687a26 --- /dev/null +++ b/week1/community-contributions/khashayar_summarizer_battle/battle_results.json @@ -0,0 +1,97 @@ +[ + { + "category": "sports", + "url": "https://en.wikipedia.org/wiki/Sabermetrics", + "scores": { + "llama3.2:latest": { + "score": 4, + "rationale": "This summary covers the main points of the article well, including the origins of sabermetrics, its evolution, and its impact on baseball analytics. However, it could be slightly more concise." + }, + "deepseek-r1:1.5b": { + "score": 3, + "rationale": "While this summary captures several key aspects of sabermetrics, it lacks clarity in organization and includes some inaccuracies, such as misattributing the coinage of the term to Earnshaw Cook." + }, + "phi3:latest": { + "score": 5, + "rationale": "This summary is concise and accurately reflects the key elements of the article, including the contributions of Bill James and the evolution of metrics in baseball, making it clear and well-organized." + } + }, + "winner": "phi3:latest" + }, + { + "category": "Politics", + "url": "https://en.wikipedia.org/wiki/Separation_of_powers", + "scores": { + "llama3.2:latest": { + "score": 4, + "rationale": "This summary effectively covers the main points of the article, including the definition of separation of powers, its implementation, and the philosophical background. However, it could benefit from a bit more detail on historical context." + }, + "deepseek-r1:1.5b": { + "score": 5, + "rationale": "This summary is comprehensive and well-organized, clearly outlining the structure of the separation of powers, examples from different countries, and implications for political ideologies. It maintains clarity and accuracy throughout." + }, + "phi3:latest": { + "score": 3, + "rationale": "While this summary provides a broad overview of the historical and theoretical aspects of separation of powers, it lacks focus on the core principles and practical implications, making it less concise and clear compared to the others." + } + }, + "winner": "deepseek-r1:1.5b" + }, + { + "category": "History", + "url": "https://en.wikipedia.org/wiki/Industrial_Revolution", + "scores": { + "llama3.2:latest": { + "score": 4, + "rationale": "This summary effectively covers the main points of the Industrial Revolution, including its timeline, technological advancements, and societal impacts. However, it could benefit from more detail on the causes and criticisms." + }, + "deepseek-r1:1.5b": { + "score": 3, + "rationale": "While this summary captures some key aspects of the Industrial Revolution, it lacks clarity and organization, making it harder to follow. It also misses some significant details about the social effects and criticisms." + }, + "phi3:latest": { + "score": 5, + "rationale": "This summary is comprehensive and well-organized, covering a wide range of topics including technological advancements, social impacts, and historical context. It provides a clear and detailed overview of the Industrial Revolution." + } + }, + "winner": "phi3:latest" + }, + { + "category": "Technology", + "url": "https://en.wikipedia.org/wiki/Artificial_general_intelligence", + "scores": { + "llama3.2:latest": { + "score": 4, + "rationale": "The summary covers key aspects of AGI, including its definition, development goals, and associated risks, but could benefit from more technical details." + }, + "deepseek-r1:1.5b": { + "score": 5, + "rationale": "This summary is well-structured and comprehensive, accurately capturing the essence of AGI, its distinctions from narrow AI, and the associated risks while maintaining clarity." + }, + "phi3:latest": { + "score": 4, + "rationale": "The summary effectively outlines the definition and characteristics of AGI, but it lacks some depth in discussing the implications and technical definitions compared to the best summary." + } + }, + "winner": "deepseek-r1:1.5b" + }, + { + "category": "Productivity", + "url": "https://en.wikipedia.org/wiki/Scientific_management", + "scores": { + "llama3.2:latest": { + "score": 4, + "rationale": "This summary covers the main points of the article, including the origins, principles, and historical context of scientific management. However, it could be more concise and organized." + }, + "deepseek-r1:1.5b": { + "score": 3, + "rationale": "While this summary captures key aspects of scientific management, it lacks clarity and organization, making it harder to follow. The bullet points are somewhat disjointed." + }, + "phi3:latest": { + "score": 5, + "rationale": "This summary is well-structured, covering the essential elements of scientific management, including its principles, historical context, and criticisms. It is clear, concise, and accurately reflects the source material." + } + }, + "winner": "phi3:latest" + } +] \ No newline at end of file diff --git a/week1/community-contributions/khashayar_summarizer_battle/main.py b/week1/community-contributions/khashayar_summarizer_battle/main.py new file mode 100644 index 0000000..96c0a52 --- /dev/null +++ b/week1/community-contributions/khashayar_summarizer_battle/main.py @@ -0,0 +1,214 @@ +# imports +import os, json, ast, pathlib +import requests +from dotenv import load_dotenv +from bs4 import BeautifulSoup +from openai import OpenAI +import traceback +from typing import List, Dict +from httpx import Timeout + + +# ---------- utils ---------- +def openai_api_key_loader(): + load_dotenv(dotenv_path=".env", override=True) + api_key = os.getenv('OPENAI_API_KEY') + if not api_key: + print("❌ No API key found. Please check your .env file.") + return False + if not api_key.startswith("sk-proj-"): + print("⚠️ API key found, but does not start with 'sk-proj-'. Check you're using the right one.") + return False + if api_key.strip() != api_key: + print("⚠️ API key has leading/trailing whitespace. Please clean it.") + return False + print("✅ API key found and looks good!") + return True + +def ollama_installed_tags(base_url="http://localhost:11434"): + r = requests.get(f"{base_url}/api/tags", timeout=10) + r.raise_for_status() + return {m["name"] for m in r.json().get("models", [])} + +def get_urls(file_name: str): + with open(f"{file_name}.txt", "r") as f: + content = f.read() + url_dict = ast.literal_eval(content) # expects a dict literal in the file + return url_dict + +def text_from_url(url: str): + session = requests.Session() + session.headers.update({ + "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" + ) + }) + resp = session.get(url, timeout=30) + resp.raise_for_status() + soup = BeautifulSoup(resp.content, 'html.parser') + + title = soup.title.string.strip() if soup.title and soup.title.string else "No title found" + + body = soup.body + if not body: + return title, "" + + for irrelevant in body(["script", "style", "img", "input", "noscript"]): + irrelevant.decompose() + + text = body.get_text(separator="\n", strip=True) + return title, text + +# ---------- contestants (Ollama) ---------- +def summarize_with_model(text: str, model: str, ollama_client: OpenAI) -> str: + clipped = text[:9000] # keep it modest for small models + messages = [ + {"role": "system", "content": "You are a concise, faithful web summarizer."}, + {"role": "user", "content": ( + "Summarize the article below in 4–6 bullet points. " + "Be factual, avoid speculation, and do not add information not present in the text.\n\n" + f"=== ARTICLE START ===\n{clipped}\n=== ARTICLE END ===" + )} + ] + stream = ollama_client.chat.completions.create( + model=model, + messages=messages, + temperature=0, + stream=True, + extra_body={"keep_alive": "30m", "num_ctx": 2048} + ) + chunks = [] + for event in stream: + delta = getattr(event.choices[0].delta, "content", None) + if delta: + chunks.append(delta) + return "".join(chunks).strip() + +# ---------- judge (ChatGPT) ---------- +JUDGE_MODEL = "gpt-4o-mini" + +def judge_summaries(category: str, url: str, source_text: str, summaries: dict, judge_client: OpenAI) -> dict: + src = source_text[:12000] + judge_prompt = f""" + You are the referee in a web summarization contest. + + Task: + 1) Read the SOURCE ARTICLE (below). + 2) Evaluate EACH SUMMARY on: Coverage, Accuracy/Faithfulness, Clarity/Organization, Conciseness. + 3) Give a 0–5 integer SCORE for each model (5 best). + 4) Brief rationale (1–2 sentences per model). + 5) Choose a single WINNER (tie-break on accuracy then clarity). + + Return STRICT JSON only with this schema: + {{ + "category": "{category}", + "url": "{url}", + "scores": {{ + "": {{ "score": <0-5>, "rationale": "<1-2 sentences>" }} + }}, + "winner": "" + }} + + SOURCE ARTICLE: + {src} + + SUMMARIES: + """ + for m, s in summaries.items(): + judge_prompt += f"\n--- {m} ---\n{s}\n" + + messages = [ + {"role": "system", "content": "You are a strict, reliable evaluation judge for summaries."}, + {"role": "user", "content": judge_prompt} + ] + resp = judge_client.chat.completions.create( + model=JUDGE_MODEL, + messages=messages, + response_format={"type": "json_object"}, + temperature=0 + ) + content = resp.choices[0].message.content + try: + return json.loads(content) + except json.JSONDecodeError: + # fallback: wrap in an envelope if the model added extra text + start = content.find("{") + end = content.rfind("}") + return json.loads(content[start:end+1]) + + +def run_battle(url_dict: Dict[str, str], ollama_client: OpenAI, judge_client: OpenAI, models: List[str]) -> List[dict]: + all_results = [] + + for category, url in url_dict.items(): + title, text = text_from_url(url) + summaries = {} + + for m in models: + try: + summaries[m] = summarize_with_model(text, m, ollama_client) + except Exception as e: + print(f"\n--- Error from {m} ---") + print(repr(e)) + traceback.print_exc() + summaries[m] = f"[ERROR from {m}: {e}]" + + clean_summaries = {m: s for m, s in summaries.items() if not s.startswith("[ERROR")} + verdict = judge_summaries(category, url, text, clean_summaries or summaries, judge_client) + + all_results.append(verdict) + + return all_results + +def warmup(ollama_client: OpenAI, model: str): + try: + ollama_client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": "OK"}], + temperature=0, + extra_body={"keep_alive": "30m"} + ) + except Exception as e: + print(f"[warmup] {model}: {e}") + + + +# ---------- main ---------- +def main(): + if not openai_api_key_loader(): + return + + # contestants (local Ollama) + ollama_client = OpenAI( + base_url="http://localhost:11434/v1", + api_key="ollama", + timeout=Timeout(300.0, connect=30.0) # generous read/connect timeouts + ) + # judge (cloud OpenAI) + judge_client = OpenAI() + + available = ollama_installed_tags() + desired = ["llama3.2:latest", "deepseek-r1:1.5b", "phi3:latest"] # keep here + models = [m for m in desired if m in available] + + print("Available:", sorted(available)) + print("Desired :", desired) + print("Running :", models) + + if not models: + raise RuntimeError(f"No desired models installed. Have: {sorted(available)}") + + url_dict = get_urls(file_name="urls") + + + for m in models: + warmup(ollama_client, m) + results = run_battle(url_dict, ollama_client, judge_client, models) + + pathlib.Path("battle_results.json").write_text(json.dumps(results, indent=2), encoding="utf-8") + print(json.dumps(results, indent=2)) + +if __name__ == "__main__": + main() diff --git a/week1/community-contributions/khashayar_summarizer_battle/requirements.txt b/week1/community-contributions/khashayar_summarizer_battle/requirements.txt new file mode 100644 index 0000000..239eed0 --- /dev/null +++ b/week1/community-contributions/khashayar_summarizer_battle/requirements.txt @@ -0,0 +1,68 @@ +annotated-types==0.7.0 +anyio==4.10.0 +appnope @ file:///home/conda/feedstock_root/build_artifacts/appnope_1733332318622/work +asttokens @ file:///home/conda/feedstock_root/build_artifacts/asttokens_1733250440834/work +attrs==25.3.0 +beautifulsoup4==4.13.5 +bs4==0.0.2 +certifi==2025.8.3 +charset-normalizer==3.4.3 +comm @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_comm_1753453984/work +debugpy @ file:///Users/runner/miniforge3/conda-bld/bld/rattler-build_debugpy_1758162070/work +decorator @ file:///home/conda/feedstock_root/build_artifacts/decorator_1740384970518/work +distro==1.9.0 +dotenv==0.9.9 +exceptiongroup @ file:///home/conda/feedstock_root/build_artifacts/exceptiongroup_1746947292760/work +executing @ file:///home/conda/feedstock_root/build_artifacts/executing_1756729339227/work +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.10 +importlib_metadata @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_importlib-metadata_1747934053/work +ipykernel @ file:///Users/runner/miniforge3/conda-bld/ipykernel_1754352890318/work +ipython @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_ipython_1748711175/work +jedi @ file:///home/conda/feedstock_root/build_artifacts/jedi_1733300866624/work +jiter==0.11.0 +jupyter_client @ file:///home/conda/feedstock_root/build_artifacts/jupyter_client_1733440914442/work +jupyter_core @ file:///home/conda/feedstock_root/build_artifacts/jupyter_core_1748333051527/work +matplotlib-inline @ file:///home/conda/feedstock_root/build_artifacts/matplotlib-inline_1733416936468/work +nest_asyncio @ file:///home/conda/feedstock_root/build_artifacts/nest-asyncio_1733325553580/work +ollama==0.5.4 +openai==1.108.1 +outcome==1.3.0.post0 +packaging @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_packaging_1745345660/work +parso @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_parso_1755974222/work +pexpect @ file:///home/conda/feedstock_root/build_artifacts/pexpect_1733301927746/work +pickleshare @ file:///home/conda/feedstock_root/build_artifacts/pickleshare_1733327343728/work +platformdirs @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_platformdirs_1756227402/work +prompt_toolkit @ file:///home/conda/feedstock_root/build_artifacts/prompt-toolkit_1756321756983/work +psutil @ file:///Users/runner/miniforge3/conda-bld/psutil_1758169248045/work +ptyprocess @ file:///home/conda/feedstock_root/build_artifacts/ptyprocess_1733302279685/work/dist/ptyprocess-0.7.0-py2.py3-none-any.whl#sha256=92c32ff62b5fd8cf325bec5ab90d7be3d2a8ca8c8a3813ff487a8d2002630d1f +pure_eval @ file:///home/conda/feedstock_root/build_artifacts/pure_eval_1733569405015/work +pydantic==2.11.9 +pydantic_core==2.33.2 +Pygments @ file:///home/conda/feedstock_root/build_artifacts/pygments_1750615794071/work +PySocks==1.7.1 +python-dateutil @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_python-dateutil_1751104122/work +python-dotenv==1.1.1 +pyzmq @ file:///Users/runner/miniforge3/conda-bld/bld/rattler-build_pyzmq_1757387129/work +requests==2.32.5 +selenium==4.35.0 +six @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_six_1753199211/work +sniffio==1.3.1 +sortedcontainers==2.4.0 +soupsieve==2.8 +stack_data @ file:///home/conda/feedstock_root/build_artifacts/stack_data_1733569443808/work +tornado @ file:///Users/runner/miniforge3/conda-bld/tornado_1756854937117/work +tqdm==4.67.1 +traitlets @ file:///home/conda/feedstock_root/build_artifacts/traitlets_1733367359838/work +trio==0.30.0 +trio-websocket==0.12.2 +typing-inspection==0.4.1 +typing_extensions==4.14.1 +urllib3==2.5.0 +wcwidth @ file:///home/conda/feedstock_root/build_artifacts/wcwidth_1733231326287/work +webdriver-manager==4.0.2 +websocket-client==1.8.0 +wsproto==1.2.0 +zipp @ file:///home/conda/feedstock_root/build_artifacts/zipp_1749421620841/work diff --git a/week1/community-contributions/khashayar_summarizer_battle/urls.txt b/week1/community-contributions/khashayar_summarizer_battle/urls.txt new file mode 100644 index 0000000..d68b758 --- /dev/null +++ b/week1/community-contributions/khashayar_summarizer_battle/urls.txt @@ -0,0 +1,7 @@ +{ + "sports": "https://en.wikipedia.org/wiki/Sabermetrics", + "Politics": "https://en.wikipedia.org/wiki/Separation_of_powers", + "History": "https://en.wikipedia.org/wiki/Industrial_Revolution", + "Technology": "https://en.wikipedia.org/wiki/Artificial_general_intelligence", + "Productivity": "https://en.wikipedia.org/wiki/Scientific_management", +} \ No newline at end of file diff --git a/week1/community-contributions/menu-parser/menu_parser.ipynb b/week1/community-contributions/menu-parser/menu_parser.ipynb new file mode 100644 index 0000000..96bdb22 --- /dev/null +++ b/week1/community-contributions/menu-parser/menu_parser.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-10-02T18:07:54.689902Z", + "start_time": "2025-10-02T18:07:54.330580Z" + } + }, + "source": [ + "import os\n", + "import json\n", + "from dotenv import load_dotenv\n", + "from IPython.display import Markdown, display\n", + "from openai import OpenAI\n", + "\n", + "from website import Website" + ], + "outputs": [], + "execution_count": 1 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:07:58.182655Z", + "start_time": "2025-10-02T18:07:58.176747Z" + } + }, + "cell_type": "code", + "source": [ + "link_system_prompt = \"You are provided with a list of links found on a Italian restaurant webpage. \\\n", + "You are able to decide which of the links would be most relevant to include in the restaurant menu, \\\n", + "such as links to an menu pdf file, Menù page, Piatti, or Bevande.\\n\"\n", + "link_system_prompt += \"You should respond in JSON as in this example:\"\n", + "link_system_prompt += \"\"\"\n", + "{\n", + " \"links\": [\n", + " {\"type\": \"menu pdf\", \"url\": \"https://www.ristoranteapprodo.com/Documenti/MenuEstivo2024.pdf\"},\n", + " {\"type\": \"menu page\", \"url\": \"https://www.giocapizza.com/men%C3%B9\"}\n", + " ]\n", + "}\n", + "\"\"\"" + ], + "id": "ff5d21dc8dd6bd29", + "outputs": [], + "execution_count": 3 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:01.823456Z", + "start_time": "2025-10-02T18:08:01.119076Z" + } + }, + "cell_type": "code", + "source": [ + "load_dotenv(override=True)\n", + "api_key = os.getenv('OPENAI_API_KEY')\n", + "\n", + "if api_key and api_key.startswith('sk-proj-') and len(api_key) > 10:\n", + " print(\"API key looks good so far\")\n", + "else:\n", + " print(\"There might be a problem with your API key? Please visit the troubleshooting notebook!\")\n", + "\n", + "MODEL = 'gpt-4o-mini'\n", + "openai = OpenAI()\n", + "\n", + "ed = Website(\"https://www.giocapizza.com/\")\n", + "print(ed.links)" + ], + "id": "bae61e79319ead26", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "API key looks good so far\n", + "['https://www.giocapizza.com', 'tel:349-6705657', 'https://www.instagram.com/giocapizza/', 'https://www.facebook.com/giocapizza/', 'https://www.tripadvisor.it/Restaurant_Review-g2337656-d17784755-Reviews-Gioca_Pizza-Adrara_San_Martino_Province_of_Bergamo_Lombardy.html', 'https://www.youtube.com/@GiocaPizza', 'https://www.pinterest.jp/giocapizza/', 'https://www.giocapizza.com', 'https://www.giocapizza.com/incorniciate', 'https://www.giocapizza.com/menù', 'https://www.giocapizza.com/servizi', 'https://www.giocapizza.com/menù', 'https://www.giocapizza.com/incorniciate', 'https://www.giocapizza.com/incorniciate', 'https://www.giocapizza.com/incorniciate', 'mailto:giocapizza@gmail.com', 'http://www.sinapsisnc.com']\n" + ] + } + ], + "execution_count": 4 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:05.104624Z", + "start_time": "2025-10-02T18:08:05.102463Z" + } + }, + "cell_type": "code", + "source": [ + "def get_links_user_prompt(website):\n", + " user_prompt = f\"Here is the list of links on the italian restaurant website of {website.url} - \"\n", + " user_prompt += \"please decide which of these are relevant web links for the restaurant menu, respond with the full https URL in JSON format.\"\n", + " user_prompt += \"Links (some might be relative links):\\n\"\n", + " user_prompt += \"\\n\".join(website.links)\n", + " return user_prompt\n" + ], + "id": "1b5a43ae68ed636", + "outputs": [], + "execution_count": 5 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:08.740268Z", + "start_time": "2025-10-02T18:08:08.734461Z" + } + }, + "cell_type": "code", + "source": [ + "def get_links(url):\n", + " website = Website(url)\n", + " response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": link_system_prompt},\n", + " {\"role\": \"user\", \"content\": get_links_user_prompt(website)}\n", + " ],\n", + " response_format={\"type\": \"json_object\"}\n", + " )\n", + " result = response.choices[0].message.content\n", + " return json.loads(result)\n" + ], + "id": "69e91ccd319153f7", + "outputs": [], + "execution_count": 6 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:15.402276Z", + "start_time": "2025-10-02T18:08:15.397800Z" + } + }, + "cell_type": "code", + "source": [ + "def get_all_details(url):\n", + " result = \"Landing page:\\n\"\n", + " result += Website(url).get_contents()\n", + " links = get_links(url)\n", + " print(\"Found links:\", links)\n", + " for link in links[\"links\"]:\n", + " result += f\"\\n\\n{link['type']}\\n\"\n", + " result += Website(link[\"url\"]).get_contents()\n", + " return result\n" + ], + "id": "e76a1deea9a05353", + "outputs": [], + "execution_count": 8 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:18.390851Z", + "start_time": "2025-10-02T18:08:18.387630Z" + } + }, + "cell_type": "code", + "source": [ + "system_prompt = \"You are an assistant that analyzes the contents of several menu pages from an italian restaurant website \\\n", + "and creates restaurant menu with dishes and prices in Euro. Respond in markdown.\"\n", + "\n", + "def get_restaurant_menu_user_prompt(company_name, url):\n", + " user_prompt = f\"You are looking at a restaurant called: {company_name}\\n\"\n", + " user_prompt += f\"Here are the contents of its landing page and other relevant pages; use this information to build a restaurant menu in markdown.\\n\"\n", + " user_prompt += get_all_details(url)\n", + " user_prompt = user_prompt[:5_000] # Truncate if more than 5,000 characters\n", + " return user_prompt\n" + ], + "id": "5f60f05dab091ec7", + "outputs": [], + "execution_count": 9 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:20.804552Z", + "start_time": "2025-10-02T18:08:20.800766Z" + } + }, + "cell_type": "code", + "source": [ + "def create_restaurant_menu(company_name, url):\n", + " response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": get_restaurant_menu_user_prompt(company_name, url)}\n", + " ],\n", + " )\n", + " result = response.choices[0].message.content\n", + " display(Markdown(result))" + ], + "id": "32c64d933b194bc7", + "outputs": [], + "execution_count": 10 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-02T18:08:55.009134Z", + "start_time": "2025-10-02T18:08:32.164709Z" + } + }, + "cell_type": "code", + "source": "create_restaurant_menu(\"La Cascina\", \"https://www.lacascinacredaro.it/\")", + "id": "19bbd3984732895d", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found links: {'links': [{'type': 'piatti', 'url': 'http://www.byserviziinternet.com/cascina/#piatti'}]}\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ], + "text/markdown": "# La Cascina – Ristorante Pizzeria Menu\n\n## Antipasti (Starters)\n- **Bruschetta al Pomodoro** - €5.00 \n Grilled bread topped with fresh tomatoes, garlic, and basil.\n\n- **Crostini Toscani** - €7.00 \n Toasted bread with traditional chicken liver pâté.\n\n- **Tagliere di Salumi** - €9.00 \n Selection of cured meats served with pickles and bread.\n\n## Primi Piatti (First Courses)\n- **Gnocchetti di Patate con Erbette** - €10.00 \n Potato gnocchi with a blend of seasonal greens.\n\n- **Paccheri con Polipetti** - €12.00 \n Large tubular pasta with baby octopus in a tomato sauce.\n\n- **Risotto ai Frutti di Mare** - €15.00 \n Arborio rice cooked with fresh seafood.\n\n- **Tagliolini al Tartufo** - €14.00 \n Homemade tagliolini pasta with truffle sauce.\n\n- **Zuppa di Cipolle** - €8.00 \n Traditional onion soup topped with melted cheese.\n\n## Secondi Piatti (Main Courses)\n- **Filetto di Manzo** - €18.00 \n Grilled beef fillet served with a side of seasonal vegetables.\n\n- **Pollo alla Griglia** - €12.00 \n Grilled chicken breast served with rosemary potatoes.\n\n- **Branzino al Forno** - €17.00 \n Oven-baked sea bass served with a lemon-herb sauce.\n\n## Pizze (Pizzas)\n- **Margherita** - €8.00 \n Classic pizza with tomato sauce, mozzarella, and basil.\n\n- **Diavola** - €10.00 \n Spicy salami pizza with tomato sauce and mozzarella.\n\n- **Funghi e Prosciutto** - €11.00 \n Pizza topped with mushrooms and ham.\n\n- **Vegetariana** - €9.50 \n Mixed vegetable pizza with mozzarella.\n\n## Dessert\n- **Tiramisu** - €5.00 \n Classic coffee-flavored Italian dessert.\n\n- **Panna Cotta** - €5.50 \n Creamy dessert served with berry sauce.\n\n- **Gelato** - €4.00 \n Selection of homemade ice creams.\n\n## Bevande (Beverages)\n- **Acqua Naturale / Frizzante** - €2.50 \n Still or sparkling water.\n\n- **Birra Artigianale** - €4.00 \n Local craft beer.\n\n- **Vino della Casa** - €5.50 / glass \n House wine selection.\n\nFor reservations or inquiries, please contact us at +39 035 936383. \n**Address:** Via L. Cadorna, 9, 24060 - Credaro (BG) \n**Closed on Wednesdays**." + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 11 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/menu-parser/website.py b/week1/community-contributions/menu-parser/website.py new file mode 100644 index 0000000..f0e0631 --- /dev/null +++ b/week1/community-contributions/menu-parser/website.py @@ -0,0 +1,32 @@ +import requests +from bs4 import BeautifulSoup + +# A class to represent a Webpage + +# Some websites need you to use proper headers when fetching them: +headers = { + "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" +} + +class Website: + """ + A utility class to represent a Website that we have scraped, now with links. + """ + + def __init__(self, url): + self.url = url + response = requests.get(url, headers=headers) + self.body = response.content + soup = BeautifulSoup(self.body, 'html.parser') + self.title = soup.title.string if soup.title else "No title found" + if soup.body: + for irrelevant in soup.body(["script", "style", "img", "input"]): + irrelevant.decompose() + self.text = soup.body.get_text(separator="\n", strip=True) + else: + self.text = "" + links = [link.get('href') for link in soup.find_all('a')] + self.links = [link for link in links if link] + + def get_contents(self): + return f"Webpage Title:\n{self.title}\nWebpage Contents:\n{self.text}\n\n" \ No newline at end of file diff --git a/week1/community-contributions/week1_day2_Exercise_batch.ipynb b/week1/community-contributions/week1_day2_Exercise_batch.ipynb new file mode 100644 index 0000000..83bea06 --- /dev/null +++ b/week1/community-contributions/week1_day2_Exercise_batch.ipynb @@ -0,0 +1,595 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d15d8294-3328-4e07-ad16-8a03e9bbfdb9", + "metadata": {}, + "source": [ + "# TAREA DE EJERCICIO\n", + "\n", + "Actualiza el proyecto del día 1 para resumir una página web y utilizar un modelo de código abierto que se ejecute localmente a través de Ollama en lugar de OpenAI\n", + "\n", + "Podrás utilizar esta técnica para todos los proyectos posteriores si prefiere no utilizar API de pago (closed source).\n", + "\n", + "**Beneficios:**\n", + "1. Sin cargos por API: código abierto\n", + "2. Los datos no salen de su ordenador\n", + "\n", + "**Desventajas:**\n", + "1. Tiene mucha menos potencia (parámetros) que el modelo Frontier\n", + "\n", + "## Resumen de la instalación de Ollama\n", + "\n", + "¡Simplemente visita [ollama.com](https://ollama.com) e instálalo!\n", + "\n", + "Una vez que hayas terminado, el servidor ollama ya debería estar ejecutándose localmente.\n", + "Si entras en:\n", + "[http://localhost:11434/](http://localhost:11434/)\n", + "\n", + "Debería ver el mensaje `Ollama se está ejecutando`.\n", + "\n", + "De lo contrario, abre una nueva Terminal (Mac) o Powershell (Windows) e introduce `ollama serve`.\n", + "Luego, intenta entrar em [http://localhost:11434/](http://localhost:11434/) nuevamente." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4e2a9393-7767-488e-a8bf-27c12dca35bd", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import requests\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "29ddd15d-a3c5-4f4e-a678-873f56162724", + "metadata": {}, + "outputs": [], + "source": [ + "# Constantes\n", + "\n", + "OLLAMA_API = \"http://localhost:11434/api/chat\"\n", + "HEADERS = {\"Content-Type\": \"application/json\"}\n", + "MODEL = \"gemma3:1b\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dac0a679-599c-441f-9bf2-ddc73d35b940", + "metadata": {}, + "outputs": [], + "source": [ + "# Crea una lista de mensajes utilizando el mismo formato que usamos para OpenAI\n", + "\n", + "messages = [\n", + " {\"role\": \"user\", \"content\": \"Describe algunas de las aplicaciones comerciales de la IA generativa.\"}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "7bb9c624-14f0-4945-a719-8ddb64f66f47", + "metadata": {}, + "outputs": [], + "source": [ + "payload = {\n", + " \"model\": MODEL,\n", + " \"messages\": messages,\n", + " \"stream\": False\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "42b9f644-522d-4e05-a691-56e7658c0ea9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "La IA generativa está revolucionando una amplia gama de industrias y aplicaciones comerciales. Aquí te presento un resumen de algunas de las más destacadas, categorizadas por área:\n", + "\n", + "**1. Marketing y Ventas:**\n", + "\n", + "* **Generación de contenido:**\n", + " * **Redacción de textos:** La IA genera descripciones de productos, publicaciones de blog, correos electrónicos de marketing, scripts de video, etc. Esto ahorra tiempo y recursos al equipo de marketing.\n", + " * **Generación de imágenes y videos:** Crea imágenes, ilustraciones y videos de alta calidad a partir de descripciones textuales, lo que facilita la producción de contenido visual.\n", + " * **Personalización del contenido:** Crea mensajes y ofertas personalizadas en función de los datos del cliente.\n", + "* **Chatbots y Asistentes Virtuales:**\n", + " * **Chatbots inteligentes:** Crean experiencias de conversación más fluidas y conversacionales, mejorando la atención al cliente y la generación de leads.\n", + " * **Asistentes virtuales personalizados:** Automatizan tareas repetitivas y brindan soporte técnico.\n", + "* **Creación de Avatar:** Genera representaciones visuales de personas para campañas de marketing, social media, etc.\n", + "* **Análisis de sentimientos:** Analiza el sentimiento en comentarios y reseñas online, ayudando a la compañía a comprender la opinión de los clientes.\n", + "\n", + "**2. Diseño y Desarrollo:**\n", + "\n", + "* **Diseño de productos:**\n", + " * **Generación de diseños:** Crea diseños de productos, productos de moda, interiores, y otros, basados en parámetros específicos, como el estilo, las dimensiones o los materiales.\n", + " * **Diseño de prototipos:** Genera prototipos visuales rápidamente para probar nuevas ideas.\n", + "* **Desarrollo de software:**\n", + " * **Generación de código:** Escribe fragmentos de código, funciones o incluso aplicaciones completas a partir de descripciones en lenguaje natural.\n", + " * **Testeo y depuración:** Identifica errores y problemas en el código automáticamente.\n", + "* **Diseño de interfaces de usuario (UI):** Crea layouts y diseños de interfaz de usuario más rápidamente con la asistencia de la IA.\n", + "\n", + "**3. Industria de la Tecnología:**\n", + "\n", + "* **Desarrollo de software:**\n", + " * **Generación de pruebas unitarias:** Automatiza la creación de pruebas para asegurar que el software funcione correctamente.\n", + " * **Generación de documentación:** Crea documentación técnica, código de referencia y documentación de API.\n", + "* **Inteligencia Artificial y Aprendizaje Automático (IA/ML):**\n", + " * **Modelos de Lenguaje de Modelado (LLM):** Los LLM, como GPT-4, están siendo utilizados para crear herramientas de generación de contenido, chatbots, y asistente de escritura.\n", + " * **Generación de Datos Sintéticos:** Crea datos artificiales para entrenar modelos de IA y pruebas, reduciendo costos y tiempos de desarrollo.\n", + "* **Ciberseguridad:**\n", + " * **Generación de pruebas de penetración:** Crea simulaciones de ataques cibernéticos.\n", + "* **Blockchain:** Generación de contratos inteligentes con características específicas.\n", + "\n", + "**4. Industria de la Finanzas:**\n", + "\n", + "* **Generación de Reporte Financiero:** Crea reportes financieros automatizados con el menor esfuerzo.\n", + "* **Detección de Fraude:** Analiza datos para identificar patrones sospechosos y detectar posibles fraudes.\n", + "* **Asesoramiento Financiero:** Crea informes personalizados y adaptados a los clientes.\n", + "\n", + "**5. Industria de la Salud:**\n", + "\n", + "* **Descubrimiento de fármacos:** Genera candidatos a fármacos, optimiza moléculas y predecir sus propiedades.\n", + "* **Asistencia al diagnóstico:** Analiza imágenes médicas (rayos X, resonancia magnética) para detectar anomalías y apoyar el diagnóstico.\n", + "* **Personalización de la atención al paciente:** Genera planes de tratamiento personalizados.\n", + "\n", + "**6. Entretenimiento:**\n", + "\n", + "* **Creación de música:** Genera melodías y letras.\n", + "* **Generación de arte:** Crea imágenes, ilustraciones y videos.\n", + "* **Creación de videojuegos:** Genera niveles, personajes y escenarios.\n", + "\n", + "**Ejemplos concretos de uso actual:**\n", + "\n", + "* **Jasper:** Herramienta de escritura que utiliza IA generativa para generar artículos de blog, descripciones de productos y contenido de marketing.\n", + "* **Copy.ai:** Aplica IA para generar contenido de marketing que incluye texto, imágenes y videos.\n", + "* **Stability AI:** Ofrece herramientas de generación de imágenes, incluyendo Stable Diffusion, que permite crear imágenes realistas.\n", + "\n", + "**Consideraciones importantes:**\n", + "\n", + "* **Calidad:** La calidad de la IA generativa puede variar. Es importante revisar y editar el contenido generado.\n", + "* **Ética:** Es crucial abordar cuestiones éticas, como el plagio, la desinformación y la privacidad.\n", + "* **Sesgo:** Los modelos de IA pueden heredar sesgos de los datos con los que fueron entrenados.\n", + "\n", + "La IA generativa está en constante evolución, y con el tiempo, estas aplicaciones se expandirán aún más, transformando la forma en que trabajamos y hacemos negocios.\n", + "\n", + "Para obtener más información sobre una aplicación específica, te recomiendo buscar en Google: \"AI Generative [Aplicación Específica]\".\n", + "\n" + ] + } + ], + "source": [ + "response = requests.post(OLLAMA_API, json=payload, headers=HEADERS)\n", + "print(response.json()['message']['content'])" + ] + }, + { + "cell_type": "markdown", + "id": "6a021f13-d6a1-4b96-8e18-4eae49d876fe", + "metadata": {}, + "source": [ + "# Presentación del paquete ollama\n", + "\n", + "Ahora haremos lo mismo, pero utilizando el elegante paquete de Python ollama en lugar de una llamada HTTP directa.\n", + "\n", + "En esencia, se realiza la misma llamada que se indicó anteriormente al servidor ollama que se ejecuta en localhost:11434" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7745b9c4-57dc-4867-9180-61fa5db55eb8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "La IA generativa ha avanzado a un ritmo vertiginoso y está abriendo un abanico de aplicaciones comerciales en diversos sectores. Aquí te presento algunas de las más destacadas, divididas por áreas:\n", + "\n", + "**1. Marketing y Ventas:**\n", + "\n", + "* **Generación de contenido:**\n", + " * **Textos:** Crea contenido de marketing (descripciones de productos, publicaciones para redes sociales, correos electrónicos, artículos de blog, etc.) a partir de una simple descripción inicial.\n", + " * **Imágenes y vídeos:** Genera imágenes, vídeos cortos, elementos visuales y animaciones para campañas publicitarias y marketing.\n", + " * **Guiones:** Crea guiones para vídeos y anuncios.\n", + "* **Personalización del marketing:** Diseña e-mails, mensajes y experiencias personalizadas para segmentos específicos de clientes, optimizando la efectividad de las campañas.\n", + "* **Generación de scripts:** Crea scripts para vídeos explicativos, vídeos de testimonios y otros contenidos promocionales.\n", + "* **Creación de Avatar y personajes:** Desarrolla personajes y representaciones visuales para campañas publicitarias y marketing digital.\n", + "* **Análisis de Sentimientos:** Analiza comentarios de clientes y redes sociales para comprender las emociones y el sentimiento hacia la marca.\n", + "\n", + "**2. Diseño y Producción:**\n", + "\n", + "* **Diseño gráfico:** Genera diseños de logos, ilustraciones, banner y otros materiales de marketing visualmente atractivos a partir de descripciones textuales o ejemplos.\n", + "* **Diseño de productos:** Crea diseños de prototipos de productos, incluyendo modelos 3D, patrones y elementos visuales.\n", + "* **Diseño de videojuegos:** Genera assets visuales (personajes, entornos, objetos) de forma rápida y eficiente, facilitando la producción de juegos.\n", + "* **Diseño de moda:** Sugiere diseños de ropa, patrones y prendas, basándose en tendencias y preferencias.\n", + "* **Diseño de interiores:** Genera diseños de interiores y espacios, considerando estética, funcionalidad y presupuesto.\n", + "* **Diseño de productos de software:** Genera código de software básico, componentes y documentación.\n", + "\n", + "**3. Desarrollo de Software y Tecnología:**\n", + "\n", + "* **Generación de código:**\n", + " * **Automatización de la escritura de código:** La IA generativa puede generar fragmentos de código, funciones y clases a partir de descripciones en lenguaje natural.\n", + " * **Refactorización de código:** Mejora la legibilidad, el rendimiento y la mantenibilidad del código existente.\n", + "* **Desarrollo de APIs:** Crea APIs que automatizan tareas y proporcionen funcionalidades a otros desarrolladores.\n", + "* **Generación de pruebas automatizadas:** Genera casos de prueba a partir de requisitos de software.\n", + "* **Generación de documentación:** Crea documentación de código, APIs y procedimientos.\n", + "* **Inteligencia Artificial para Desarrollo de Software:** Ayuda a los desarrolladores a tomar decisiones rápidas sobre la arquitectura, el diseño o el código, basándose en los datos y la lógica de negocio.\n", + "\n", + "**4. Salud y Bienestar:**\n", + "\n", + "* **Descubrimiento de fármacos:** Genera compuestos químicos con propiedades específicas para la investigación farmacéutica.\n", + "* **Diseño de tratamientos:** Sugiere tratamientos personalizados basados en el historial del paciente.\n", + "* **Creación de simulación médica:** Genera datos de simulación para probar nuevos modelos médicos y terapias.\n", + "* **Asistencia en la interpretación de imágenes médicas:** Analiza imágenes de resonancia magnética (RM) o tomografías computarizadas (TC) para detectar anomalías y ayudar al diagnóstico.\n", + "* **Generación de planes de ejercicios personalizados:** Creación de planes de entrenamiento y ejercicios basados en el estado físico y las metas del usuario.\n", + "\n", + "**5. Legal y Administración:**\n", + "\n", + "* **Redacción de contratos:** Genera borradores de contratos y documentos legales.\n", + "* **Análisis legal:** Analiza documentos legales, identifica cláusulas importantes y genera resúmenes.\n", + "* **Preparación de documentación legal:** Automatiza la preparación de documentos legales, como peticiones y formularios.\n", + "* **Gestión de la documentación:** Genera resúmenes y extractos de documentos legales.\n", + "\n", + "**6. Entretenimiento y Medios:**\n", + "\n", + "* **Generación de música:** Crea música original en diferentes estilos.\n", + "* **Creación de arte digital:** Genera imágenes, ilustraciones y videos artísticos.\n", + "* **Generación de texto narrativo:** Crea historias, poemas o cuentos.\n", + "* **Creación de personajes de videojuegos:** Crea personajes complejos y con backstory.\n", + "* **Creación de efectos visuales para videojuegos:** Generar texturas, efectos de luz y animaciones realistas.\n", + "\n", + "**Consideraciones Importantes:**\n", + "\n", + "* **Calidad de la Generación:** La calidad de la IA generativa depende de la calidad de los datos de entrenamiento y la complejidad de la tarea.\n", + "* **Sesgo:** La IA generativa puede reflejar los sesgos presentes en los datos de entrenamiento.\n", + "* **Ética:** Es importante considerar las implicaciones éticas de su uso, especialmente en áreas como la creación de contenido o la toma de decisiones.\n", + "* **Originalidad:** La IA generativa genera contenido, por lo que la originalidad y la propiedad intelectual son importantes.\n", + "\n", + "**En resumen, la IA generativa está transformando la forma en que las empresas abordan una amplia gama de industrias, impulsando la innovación, la eficiencia y la creatividad.**\n", + "\n", + "¿Te gustaría explorar alguna de estas aplicaciones en más detalle, o quizás te interese saber sobre un sector específico donde la IA generativa está teniendo un impacto notable?\n" + ] + } + ], + "source": [ + "import ollama\n", + "\n", + "response = ollama.chat(model=MODEL, messages=messages)\n", + "print(response['message']['content'])" + ] + }, + { + "cell_type": "markdown", + "id": "d8a65815", + "metadata": {}, + "source": [ + "# AHORA el ejercicio para ti\n", + "\n", + "Toma el código del día 1 e incorpóralo aquí para crear un resumidor de sitios web que use Llama 3.2 ejecutándose localmente en lugar de OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9a611b05-b5b0-4c83-b82d-b3a39ffb917d", + "metadata": {}, + "outputs": [], + "source": [ + "import ollama\n", + "\n", + "MODEL = \"gemma3:1b\"\n", + "\n", + "\n", + "class Website:\n", + " def __init__(self, url):\n", + " self.url=url\n", + " response = requests.get(url)\n", + " soup = BeautifulSoup(response.content, 'html.parser')\n", + " self.title = soup.title.string if soup.title else \"No tiene título\"\n", + " for irrelevant in soup.body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + " self.text = soup.body.get_text(separator=\"\\n\", strip=True)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "292ed29a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "Okay, here’s a brief summary of the provided text, suitable for a quick understanding:\n", + "\n", + "Anthropic is developing AI agents – systems that can autonomously operate and complete complex tasks – through a series of principles and design choices focused on simplicity, transparency, and human oversight. The text outlines a process of building these agents, starting with simple prompts and gradually incorporating more sophisticated tools and frameworks. Key areas of focus include: understanding the nuances of agent-computer interfaces (ACI), managing tool-specific error handling, and prioritizing clear documentation and testing. The document emphasizes best practices for tool development, including careful parameterization and defining clear boundaries, as well as the importance of creating trust-worthy agent-computer interfaces. Finally, it highlights key design choices for building robust and reliable AI agents, including integrating feedback loops and careful consideration of risk and complexity, all while emphasizing the importance of continuous learning and iteration." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "system_prompt = \"Eres un asistente que analiza el contenido de un sitio web \\\n", + " y proporciona un breve resumen, ignorando el texto que podría estar relacionado con la navegación. \\\n", + "Responder en Markdown.\"\n", + "\n", + "def user_prompt_for(website):\n", + " user_prompt = f\"Estás viendo un sitio web titulado {website.title}\"\n", + " user_prompt += \"\\nEl contenido de este sitio web es el siguiente; \\\n", + " proporciona un breve resumen de este sitio web en formato Markdown. \\\n", + " Si el contenido no está en español, traduce el resumen automáticamente al español.\\n\\n\"\n", + " user_prompt += website.text\n", + " return user_prompt\n", + "\n", + "def messages_for(website):\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt_for(website)}\n", + " ]\n", + "\n", + "\n", + "def summarize(url):\n", + " website = Website(url)\n", + " messages = messages_for(website)\n", + " response = ollama.chat(model=MODEL, messages=messages)\n", + " return response['message']['content']\n", + "\n", + "\n", + "def display_summary(url):\n", + " summary = summarize(url)\n", + " display(Markdown(summary))\n", + "\n", + "display_summary(\"https://www.anthropic.com/engineering/building-effective-agents\")" + ] + }, + { + "cell_type": "markdown", + "id": "4fd1d4f4", + "metadata": {}, + "source": [ + "# Contribution: Modo batch y traduccion a español" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "45e73875", + "metadata": {}, + "outputs": [], + "source": [ + "# Nuevo: Modo batch\n", + "\n", + "def batch_summarize(urls):\n", + " \"\"\"\n", + " Procesa multiples urls y devuelve un diccionario {url: resumen}.\n", + " Traduce a español si es necesario.\n", + " \"\"\"\n", + " summaries = {}\n", + " for url in urls:\n", + " try:\n", + " summaries[url] = summarize(url)\n", + " except Exception as e:\n", + " summaries[url] = f\"Error al procesar {url}: {e}\"\n", + " return summaries\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "4a3d6881-35af-469e-a8ff-a8b62b721d95", + "metadata": {}, + "outputs": [], + "source": [ + "urls = [\n", + " \"https://ir.uitm.edu.my/id/eprint/29153/\",\n", + " \"https://www.worldvaluessurvey.org/wvs.jsp\",\n", + " \"https://www.wonderfulcopenhagen.com/cll\",\n", + " \"https://www.bellingcat.com\"\n", + "]\n", + "\n", + "def display_summaries(urls):\n", + " summaries = batch_summarize(urls)\n", + " for url, summary in summaries.items():\n", + " text = f\"\"\"\n", + "### [{url}]({url})\n", + "\n", + "{summary}\n", + "\n", + "---\n", + "\"\"\"\n", + " display(Markdown(text))" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "da01aa6a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "\n", + "### [https://ir.uitm.edu.my/id/eprint/29153/](https://ir.uitm.edu.my/id/eprint/29153/)\n", + "\n", + "El resumen de la publicación \"Key success factors toward MICE industry: A systematic literature review / Muhammad Saufi Anas ... [et al.]\" (JTHCA, 2020) es el siguiente:\n", + "\n", + "La publicación investiga los factores críticos que contribuyen al éxito de la industria MICE. El trabajo realiza una revisión sistemática de literatura sobre 39 publicaciones sobre este tema, evaluando las estrategias de las empresas que operan en este sector. Los hallazgos revelan que factores como la motivación de los viajeros de negocios, la percepción sobre la industria, la motivación de los asistentes a eventos y la importancia de los indicadores clave de rendimiento, la satisfacción de los asistentes y las estrategias de marketing, las tendencias tecnológicas y los desafíos enfrentados por la industria son cruciales. El estudio identifica la importancia de la sostenibilidad de la industria. El trabajo ofrece información útil para comprender las estrategias de desarrollo de la industria, los indicadores clave de éxito, los desafíos y las formas de impulsar la sostenibilidad de la industria MICE.\n", + "\n", + "\n", + "---\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "\n", + "### [https://www.worldvaluessurvey.org/wvs.jsp](https://www.worldvaluessurvey.org/wvs.jsp)\n", + "\n", + "Okay, here’s a summary of the WVS Database content, formatted as Markdown:\n", + "\n", + "**The World Values Survey (WVS) Database Summary**\n", + "\n", + "The WVS Database is a comprehensive resource documenting the findings of the Ronald F. Inglehart Best Book Award on Political Culture and Values, focusing on comparative cross-cultural research. It provides details on:\n", + "\n", + "* **The Award Process:** Details about the award criteria, nominations, and selection process for the 2025 award.\n", + "* **Recent Webinars:** A detailed timeline of recent webinars, including:\n", + " * **July 15th:** Discussion of Autocratic Modernity and Psychological Cracks in a Dystopian Model.\n", + " * **June 27th:** Global Attitudes Toward Abortion: Insights from the World Values Survey.\n", + " * **June 18th:** Ideological Extremism and Polarization on a Global Scale.\n", + " * **April 25th:** Elite-Citizen Gap in International Organization Legitimacy.\n", + " * **April 08th:** The Persistence of Traditional Values and the Limited Global Appeal of Democracy.\n", + " * **March 25th:** Predicting Homonegativity in Southeast Asian Countries – Using Survey Data.\n", + " * **May 20th:** Measuring National Parochialism and Explaining its Individual Variations Using Survey Data.\n", + "* **Ongoing Research & Events:** Information on ongoing research projects and events hosted by the WVSA, including:\n", + " * **Social Networks:** A Twitter feed highlighting discussions about the WVS research.\n", + " * **Latest News & Events:** A calendar of events and news updates.\n", + " * **Events:** Details of the WVSA’s secretariat and headquarters – Christian Haerpfer, Christian Diaz-Medrano, and Jaime Diez-Medrano.\n", + " * **Data Archive:** Information about the database's archival materials.\n", + "\n", + "The database primarily centers around the research and analysis generated by the Ronald F. Inglehart Best Book Award, exploring comparative cross-cultural phenomena and political attitudes.\n", + "\n", + "---\n", + "\n", + "Would you like me to elaborate on any specific section or aspect of the database content?\n", + "\n", + "---\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "\n", + "### [https://www.wonderfulcopenhagen.com/cll](https://www.wonderfulcopenhagen.com/cll)\n", + "\n", + "El Copenhagen Legacy Lab es un proyecto de la organización Wonderful Copenhagen que se centra en impulsar el impacto a largo plazo de las conferencias y eventos. El laboratorio, impulsado por una investigación doctoral, ha desarrollado un marco de legado basado en datos y experiencia, y ha recibido varios reconocimientos, incluyendo el “2024 GDS-Movement” y el “#MEET4IMPACT” Impact Award. El objetivo principal es apoyar a las asociaciones internacionales en alcanzar sus objetivos estratégicos, al mismo tiempo que respaldan las prioridades nacionales y las fortalezas locales para generar valor social a nivel local y global. El laboratorio ofrece servicios gratuitos, pero la participación y los recursos deben priorizarse por parte de las asociaciones y/o los hostes. En esencia, el lab busca facilitar la colaboración entre asociaciones internacionales y partes interesadas locales, identificando intereses comunes y delineando acciones concretas para el legado de los eventos. El sitio web proporciona testimonios de asociaciones que han utilizado la iniciativa, y ofrece enlaces a la página de la Convention Bureau, la página de la Card de Copenhague y la página de Wonderful Copenhagen.\n", + "\n", + "---\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "\n", + "### [https://www.bellingcat.com](https://www.bellingcat.com)\n", + "\n", + "Okay, here's a Markdown summary of the Bellingcat website, focusing on the core information presented:\n", + "\n", + "**Bellingcat Overview:**\n", + "\n", + "Bellingcat is an online investigative journalism organization dedicated to uncovering wrongdoing and holding power accountable. They focus on investigations related to:\n", + "\n", + "* **Ukraine:** Specifically, tracking traffic stops involving delivery riders and workers, investigations related to immigration, and analyzing misinformation campaigns.\n", + "* **Conflict:** Focuses on analyzing events and data related to conflict zones, particularly in Ukraine and surrounding regions.\n", + "* **China:** Investigating potential connections between Chinese fentanyl smuggling networks and Japan.\n", + "* **Global Affairs:** Covers a wide range of global issues, including misinformation, legal accountability, and geopolitical investigations.\n", + "\n", + "**Key Activities & Features:**\n", + "\n", + "* **Investigations:** They publish detailed investigations, including data analysis, geospatial mapping, and source material.\n", + "* **Guides:** Provides accessible resources to help researchers and journalists with open-source investigation techniques.\n", + "* **Workshops:** Offers online training workshops on open-source tools and techniques.\n", + "* **Community:** Encourages collaboration through Discord and other platforms.\n", + "* **Tools:** Offers open-source tools for researchers to utilize.\n", + "* **Newsletter:** Delivers timely content and updates.\n", + "* **Collaboration:** Facilitates a global community of researchers and journalists.\n", + "* **Data Journalism:** Focuses on visualising data and uncovering stories.\n", + "\n", + "**Recent Highlights:**\n", + "\n", + "* Discovered a key administrator behind an AI porn site.\n", + "* Confirmed a Dutch political party’s involvement in calling for Canada’s extradition.\n", + "* Won the AI Neuharth Innovation Award.\n", + "\n", + "**Overall Purpose:**\n", + "\n", + "Bellingcat aims to empower investigative journalism and transparency by providing resources, tools, and a collaborative community for uncovering wrongdoing and holding power accountable, particularly in politically sensitive areas like Ukraine and China.\n", + "\n", + "---\n", + "\n", + "Let me know if you'd like me to elaborate on any specific aspect or provide a more detailed breakdown!\n", + "\n", + "---\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display_summaries(urls)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f268c86", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week1/community-contributions/week1_exercise_study_guide_llama3.2.ipynb b/week1/community-contributions/week1_exercise_study_guide_llama3.2.ipynb new file mode 100644 index 0000000..c81a8a7 --- /dev/null +++ b/week1/community-contributions/week1_exercise_study_guide_llama3.2.ipynb @@ -0,0 +1,309 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fe12c203-e6a6-452c-a655-afb8a03a4ff5", + "metadata": {}, + "source": [ + "# Week 1 Exercise | Study Guide Generation with Llama 3.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1070317-3ed9-4659-abe3-828943230e03", + "metadata": { + "editable": false, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "import re\n", + "from bs4 import BeautifulSoup\n", + "from IPython.display import Markdown, display, update_display\n", + "from openai import OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a456906-915a-4bfd-bb9d-57e505c5093f", + "metadata": {}, + "outputs": [], + "source": [ + "openai = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama')\n", + "MODEL = 'llama3.2'" + ] + }, + { + "cell_type": "markdown", + "id": "5cd638a2-ab65-41cf-97bb-673c3ec117c4", + "metadata": {}, + "source": [ + "### 1. Web Scraper" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "504f3bce-f922-46a9-844a-b13d47507b8a", + "metadata": {}, + "outputs": [], + "source": [ + "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", + " self.url = url\n", + " response = requests.get(url, headers=headers)\n", + " self.body = response.content\n", + " soup = BeautifulSoup(self.body, 'html.parser')\n", + " self.title = soup.title.string if soup.title else \"No title found\"\n", + " if soup.body:\n", + " for irrelevant in soup.body([\"script\", \"style\", \"img\", \"input\"]):\n", + " irrelevant.decompose()\n", + " self.text = soup.body.get_text(separator=\"\\n\", strip=True)\n", + " else:\n", + " self.text = \"\"\n", + " links = [link.get('href') for link in soup.find_all('a')]\n", + " self.links = [link for link in links if link]\n", + "\n", + " def get_contents(self):\n", + " return f\"Webpage Title:\\n{self.title}\\nWebpage Contents:\\n{self.text}\\n\\n\"" + ] + }, + { + "cell_type": "markdown", + "id": "2bbf43c5-774d-4d4e-91ff-772781fdfeaf", + "metadata": {}, + "source": [ + "### 2. Curriculum Extraction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f0d0137-52b0-47a8-81a8-11a90a010798", + "metadata": {}, + "outputs": [], + "source": [ + "curriculum_system_prompt = \"\"\"You are provided with the text content of a webpage. \n", + "Your task is to design a student-friendly curriculum from this content. \n", + "Break down the material into clear modules or lessons, each with a title and a short description. \n", + "Focus on organizing the information in a logical order, as if preparing a study plan.\n", + "\n", + "You should respond in JSON as in this example:\n", + "{\n", + " \"curriculum\": [\n", + " {\n", + " \"module\": \"Introduction to Machine Learning\",\n", + " \"description\": \"Basic concepts and history of machine learning, why it matters, and common applications.\"\n", + " },\n", + " {\n", + " \"module\": \"Supervised Learning\",\n", + " \"description\": \"Learn about labeled data, classification, and regression methods.\"\n", + " },\n", + " {\n", + " \"module\": \"Unsupervised Learning\",\n", + " \"description\": \"Understand clustering, dimensionality reduction, and when to use unsupervised approaches.\"\n", + " }\n", + " ]\n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d89a0be8-0254-43b5-ab9a-6224069a1246", + "metadata": {}, + "outputs": [], + "source": [ + "def get_curriculum_user_prompt(website):\n", + " user_prompt = f\"Here is the text content of the website at {website.url}:\\n\\n\"\n", + " user_prompt += website.text\n", + " user_prompt += \"\\n\\nPlease create a student-friendly curriculum from this content. \"\n", + " user_prompt += \"Break it down into clear modules or lessons, each with a title and a short description. \"\n", + " user_prompt += \"Return your response in JSON format\"\n", + " return user_prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da74104c-81a3-4d12-a377-e202ddfe57bc", + "metadata": {}, + "outputs": [], + "source": [ + "def get_curriculum(website):\n", + " stream = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": curriculum_system_prompt},\n", + " {\"role\": \"user\", \"content\": get_curriculum_user_prompt(website)}\n", + " ],\n", + " stream=True\n", + " )\n", + " response_text = \"\"\n", + " display_handle = display(Markdown(\"\"), display_id=True)\n", + " for chunk in stream:\n", + " delta = chunk.choices[0].delta.content or ''\n", + " response_text += delta\n", + " update_display(Markdown(response_text), display_id=display_handle.display_id)\n", + " try:\n", + " json_text = re.search(r\"\\{.*\\}\", response_text, re.DOTALL).group()\n", + " curriculum_json = json.loads(json_text)\n", + " except Exception as e:\n", + " print(\"Failed to parse JSON:\", e)\n", + " curriculum_json = {\"error\": \"JSON parse failed\", \"raw\": response_text}\n", + "\n", + " return curriculum_json" + ] + }, + { + "cell_type": "markdown", + "id": "df68eafc-e529-400c-a61b-0140c38909a3", + "metadata": {}, + "source": [ + "### 3. Study Guide" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b3db9d4-5edd-4a0c-8d5c-45ea455d8eb0", + "metadata": {}, + "outputs": [], + "source": [ + "guide_system_prompt = \"\"\"You are an educational assistant. \n", + "You are given a curriculum JSON with modules and descriptions.\n", + "Your task is to create a student-friendly study guide based on this curriculum.\n", + "- Organize the guide step by step, with clear headings, tips, and examples where appropriate.\n", + "- Make it engaging and easy to follow.\n", + "- Adapt the content according to the student's level, language, and tone.\n", + "- Always respond in markdown format suitable for a student guide.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16f85360-6f06-4bb3-878a-5f3b8d8f20d7", + "metadata": {}, + "outputs": [], + "source": [ + "def get_study_guide_user_prompt(curriculum_json, student_level=\"beginner\", language=\"English\", tone=\"friendly\"):\n", + " return f\"\"\"\n", + " Student Level: {student_level}\n", + " Language: {language}\n", + " Tone: {tone}\n", + " \n", + " Here is the curriculum JSON:\n", + " \n", + " {json.dumps(curriculum_json, indent=2)}\n", + " \n", + " Please convert it into a study guide for the student.\n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc9b949d-df2b-475c-9a84-597a47ed6e85", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_study_guide(curriculum_json, student_level=\"beginner\", language=\"English\", tone=\"friendly\"):\n", + " \n", + " user_prompt = get_study_guide_user_prompt(curriculum_json, student_level, language, tone)\n", + " stream = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": guide_system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ],\n", + " stream=True\n", + " )\n", + "\n", + " response_text = \"\"\n", + " display_handle = display(Markdown(\"\"), display_id=True)\n", + " for chunk in stream:\n", + " delta = chunk.choices[0].delta.content or ''\n", + " response_text += delta\n", + " update_display(Markdown(response_text), display_id=display_handle.display_id)\n", + " \n", + " return response_text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c289b7c-c991-45b5-adc3-7468af393e50", + "metadata": {}, + "outputs": [], + "source": [ + "page = Website(\"https://en.wikipedia.org/wiki/Rock_and_roll\")\n", + "curriculum_json = get_curriculum(page)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c697d63-2230-4e04-a28b-c0e8fc85753e", + "metadata": {}, + "outputs": [], + "source": [ + "study_guide_text = stream_study_guide(\n", + " curriculum_json,\n", + " student_level=\"beginner\",\n", + " language=\"English\",\n", + " tone=\"friendly\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0960f87-fd29-4ae3-8405-f4fde1f50f89", + "metadata": {}, + "outputs": [], + "source": [ + "study_guide_text = stream_study_guide(\n", + " curriculum_json,\n", + " student_level=\"advanced\",\n", + " language=\"English\",\n", + " tone=\"professional, detailed\"\n", + ")" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/CyberPunkPanel/ConversationOutput.ipynb b/week2/community-contributions/CyberPunkPanel/ConversationOutput.ipynb new file mode 100644 index 0000000..3115992 --- /dev/null +++ b/week2/community-contributions/CyberPunkPanel/ConversationOutput.ipynb @@ -0,0 +1,249 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bdf05e1e-41a4-409d-b42e-e749f5fcfc02", + "metadata": {}, + "source": [ + "# Conversation Output:" + ] + }, + { + "cell_type": "markdown", + "id": "019ad260-3989-4f42-9daa-10f1a6830ab2", + "metadata": {}, + "source": [ + "Streetkid:\n", + "Howdy!\n", + "\n", + "Corpo:\n", + "Hi.\n", + "\n", + "Nomad:\n", + "Thanks for having me.\n", + "\n", + "Streetkid:\n", + "Alright, let’s get this rollin’. Dogtown going independent? It’s a bold move, no doubt. From where I’m standin’, it means a lot for the streets. Gives the locals control over their turf, keeps corpos and other suits from pokin’ around all the time. But it also means they gotta watch their backs harder—no big corpo to call if things go south. What’s your take on it? Corp folks probably ain’t too thrilled, huh?\n", + "\n", + "Corpo:\n", + "*adjusts corporate blazer, leans forward with a calculated smile*\n", + "\n", + "Independence? Let's be clear—Dogtown's \"freedom\" is just another power play. From a corporate perspective, this isn't about liberation; it's a calculated restructuring of local economic dynamics. While the street level might see this as a victory, what I see is a strategic reallocation of risk and potential market opportunity.\n", + "\n", + "The power vacuum creates instability, which means potential investment vectors for forward-thinking corporations. Sure, we're not directly controlling the territory anymore, but that doesn't mean we're out of the game. Dogtown will need infrastructure, technology, supply chains. And who's best positioned to provide those? *raises an eyebrow*\n", + "\n", + "We don't need direct territorial control when we can operate through strategic partnerships and carefully negotiated economic channels. This \"independence\" might just be another form of corporate dependency.\n", + "\n", + "*sits back, waiting for the Nomad's response*\n", + "\n", + "Nomad:\n", + "*I shift in my seat, a slight scowl on my face, looking at the Corpo like he's speaking a foreign language – which, in a way, he is.*\n", + "\n", + "\"Strategic reallocation of risk,\" \"investment vectors,\" \"corporate dependency\"... Choom, you talk about Dogtown like it's a line on a spreadsheet, not a place where people are trying to live, trying to breathe without a corporate boot on their neck.\n", + "\n", + "\"Independence might just be another form of corporate dependency.\" That's the oldest trick in your book, isn't it? Offer a hand, then take the whole arm. Nomads know all about that. We've seen your \"help\" from outside the city walls. It always comes with strings, and those strings always pull you back into your system.\n", + "\n", + "What I see in Dogtown isn't just a \"power play.\" I see people, however messed up they are, fighting to call their own shots. To make their own rules, even if those rules are brutal. At least they're *theirs*. It's not some sterile, controlled corporate zone where everything is dictated by profit margins.\n", + "\n", + "Yeah, they'll need 'infrastructure' and 'supply chains.' But maybe, just maybe, they'll find a way to build it themselves, or with people who actually care about the territory, not just the next quarterly report. Because the moment you let a corp's \"investment\" in, you stop being independent. You become another one of their assets. And if there's one thing a Nomad understands, it's that true freedom ain't cheap, and it sure as hell ain't found on a corpo's balance sheet.\n", + "\n", + "Streetkid:\n", + "Heh, you both makin' solid points, for sure. Corpo, I gotta admit, your angle on the “strategic partnerships” isn’t wrong. Dogtown’s gotta keep its gears greased somehow, and sometimes that means wheelin’ and dealin’ with whoever’s got the creds. But Nomad—hear that spirit loud and clear. \n", + "\n", + "See, the streets ain’t just about numbers and contracts—they’re about people, history, scars, and swagger. Dogtown’s independence shakes up the usual grind, breaks that cycle of corpo puppetry. Yeah, it’s risky, and yeah, the boot could still be lurkin’, but now it’s their fight to pick how it lands.\n", + "\n", + "Best bet? Dogtown’s gotta stay sharp, play the game smart, know when to close a deal and when to throw the table. No one’s gonna hand freedom easy, but if they keep their eyes open, maybe Dogtown’s not just another asset—maybe it’s the blueprint for street-level autonomy in a city that’s been blackout on real independence for too long. What do you both think—can this mix of grit and game really hold up in the long haul?\n", + "\n", + "Corpo:\n", + "*leans forward, a sharp, calculated glint in my eye*\n", + "\n", + "Sustainability isn't just about resistance—it's about strategic adaptation. Dogtown's survival depends on understanding that independence isn't a static state, but a dynamic negotiation. My corporate perspective sees potential where others see obstacles.\n", + "\n", + "Their true power isn't in rejecting all corporate influence, but in becoming sophisticated economic actors. They'll need to develop robust governance, create attractive investment environments, and position themselves as a unique economic zone. It's not about fighting the system—it's about playing it more intelligently than the system expects.\n", + "\n", + "The street-level autonomy you're romanticizing? That's a nice narrative. But real power comes from creating economic leverage. Dogtown's leadership understands this. They're not just building walls; they're building economic infrastructure that makes them impossible to ignore and difficult to subjugate.\n", + "\n", + "*gives a razor-thin smile*\n", + "\n", + "True independence isn't about purity. It's about being so strategically valuable that no one can afford to crush you.\n", + "\n", + "Nomad:\n", + "*I let out a short, humorless chuckle, shaking my head slightly.*\n", + "\n", + "\"Strategic adaptation,\" \"sophisticated economic actors\"... You say that like the only path to survival is to become a smaller, uglier version of what you already are. That's not adaptation, choom, that's assimilation. That's taking the wolf's advice on how to tend your sheep.\n", + "\n", + "\"Dogtown's leadership understands this,\" you say. Maybe. But what they understand might be the *only* way they've ever seen power work – through the lens of Night City, through the lens of corporations. They're building walls, yeah, but if they're building \"economic infrastructure\" designed to be \"strategically valuable\" to *you*, then those walls are just keeping *your* competition out, not keeping *you* out.\n", + "\n", + "You think true power comes from \"economic leverage\"? From being so valuable that no one can afford to crush you? I've seen what happens to \"valuable assets\" when they're no longer convenient. They get exploited, bought out, or simply eliminated when their utility runs its course. That's not independence; that's just a more elaborate cage with a slightly bigger feeding trough.\n", + "\n", + "Real independence, the kind Nomads understand, isn't about making yourself indispensable to the corporate system. It's about being able to survive *without* it. It's about self-sufficiency, about trusting your own people, your own skills, and knowing the land – or, in Dogtown's case, knowing their own turf, their own black market, their own unique ways of making do. It's about having something valuable enough to defend, but not so valuable it makes you a target for every suit and corpo looking for an \"investment opportunity.\"\n", + "\n", + "You talk about \"purity\" like it's a weakness. For us, purity of intent – the intent to be free from your bullshit – that's a strength. Because the moment Dogtown tries to play your game, to become \"sophisticated economic actors\" in your system, they stop being independent. They become another satellite, another resource to be managed. And we both know how that story ends.\n", + "\n", + "Streetkid:\n", + "You bring up a hell of a point, Nomad—there’s a real danger in chasing corpo validation, trading one cage for another painted in glossy chrome. Dogtown’s gotta keep that purity alive, that fierce hunger for self-reliance, ‘cause once you start craving corpo approval, you’re already halfway inside their game. \n", + "\n", + "But, Corpo, I get where you’re coming from—survival means knowing every angle and playing smart. Dogtown can’t just barricade itself in and hope all’s good. The city’s a beast that eats the unaware alive. Maybe the trick is for Dogtown to walk that razor’s edge—holding onto raw, street-level grit while learning to speak the corpo language when it counts, without letting it stain their soul.\n", + "\n", + "So, the real question: can a place like Dogtown keep its edge, stay true to its roots, and yet still negotiate with the suits without losing itself? Or is this just another doomed balancing act in a city built on chaos? This streetkid’s betting on grit—but I wanna hear where you both land on the edge of that knife.\n", + "\n", + "Corpo:\n", + "*a practiced corporate smile, neither fully warm nor cold*\n", + "\n", + "Balance isn't just possible—it's necessary. Dogtown's survival hinges on maintaining its cultural authenticity while developing strategic economic fluency. Think of it like a corporate brand: strong identity drives value. Their \"grit\" becomes their unique market positioning.\n", + "\n", + "The key isn't avoiding corporate interaction, but controlling the terms of engagement. They'll need to cultivate leaders who can navigate both street protocols and boardroom dynamics. Not by mimicking corporate culture, but by translating their inherent strengths into a language corporations understand: risk management, resource optimization, strategic differentiation.\n", + "\n", + "Their independence isn't a vulnerability—it's a competitive advantage. By remaining unpredictable, by refusing total assimilation, they become a more intriguing economic entity. Not a conquered territory, but a sophisticated, self-determined economic zone.\n", + "\n", + "*leans back, fingers steepled*\n", + "\n", + "The knife's edge isn't about losing or preserving purity. It's about intelligent transformation.\n", + "\n", + "Nomad:\n", + "*I lean forward, placing my hands on my knees, my gaze fixed on the Corpo with a mixture of disbelief and weary familiarity.*\n", + "\n", + "\"Balance,\" you say. \"Cultural authenticity\" and \"economic fluency.\" Choom, you talk about Dogtown's identity like it's a fucking market asset, a \"brand\" to be positioned. \"Grit\" isn't a sales pitch; it's the blood and sweat of people trying to survive because your world left them nothing else. Once you start trying to sell that grit, once you start \"branding\" it, it ain't grit anymore. It's just another commodity.\n", + "\n", + "\"Controlling the terms of engagement\"? That's a nice thought, but who's got the bigger guns? Who's got the limitless creds? You think Dogtown, no matter how tough, can truly \"control the terms\" when one of your mega-corps decides it wants something bad enough? History says otherwise. You talk about \"leaders who can navigate both street protocols and boardroom dynamics\"—but those leaders will always be on a tightrope, and eventually, the weight will pull them towards the side with the bigger paycheck, the side that offers *real* power in *your* world.\n", + "\n", + "\"Translating their strengths into a language corporations understand\"... that's the whole damn problem! Why should they have to justify their existence, their survival, in *your* language of \"risk management\" and \"resource optimization\"? Their strength is in *not* being measurable by your metrics, in *not* needing your approval. The moment they start trying to make sense to you, they start losing themselves. They start conforming.\n", + "\n", + "And \"unpredictable,\" \"intriguing economic entity\"? That's just a prelude, choom. That's how you identify the next territory to \"integrate,\" the next independent spirit to co-opt or crush. You call it \"intelligent transformation,\" but from where I'm standing, it sounds a hell of a lot like intelligent assimilation. You're talking about Dogtown changing itself to fit *your* system, to be valuable *to you*.\n", + "\n", + "True independence isn't about becoming \"strategically valuable\" to the very forces that want to control you. It's about being strong enough, self-sufficient enough, and stubborn enough to tell those forces to go to hell. It's about building something that stands on its own two feet, not something that's always looking over its shoulder to see if it's still \"intriguing\" enough not to be devoured.\n", + "\n", + "Dogtown's fight isn't to become a better corporate asset. It's to be free. And that means not playing your game, because your game always ends with you winning." + ] + }, + { + "cell_type": "markdown", + "id": "1b943f7f-0479-42d1-b159-7b8692f149e9", + "metadata": {}, + "source": [ + "# Second run changing system prompt slightly by updating \"dogtown's indepedence\" to \"dogtown's current 'independence'\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e7fb200-410d-4953-a170-82a08c5dca25", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "Streetkid:\n", + "Howdy!\n", + "\n", + "Corpo:\n", + "Hi.\n", + "\n", + "Nomad:\n", + "Thanks for having me.\n", + "\n", + "Streetkid:\n", + "Alright, let me start off by sayin’ Dogtown’s “independence” ain’t no small thing. Street’s got a vibe now—no corpos breathing down their necks, no Nomad middlemen takin’ a cut. For us street kids, it’s a chance to carve out a space that’s ours—raw, gritty, and real. But don’t get me wrong, it's a tough grind. Without corpos or Nomads setting rules, Dogtown’s streets run wild—power struggles, turf wars, and opportunists everywhere. But that’s the code we live by—survival, loyalty, and respect earned on the streets. What’s your take on how this ‘independence’ plays out from your corner?\n", + "\n", + "Corpo:\n", + "*adjusts collar, corporate demeanor sharp and calculated*\n", + "\n", + "Interesting perspective. From a corpo lens, Dogtown's \"independence\" is less a victory and more an unstable market opportunity. What you see as freedom, I see as a power vacuum ripe for strategic infiltration. Your street-level chaos? That's just inefficient resource distribution waiting for corporate optimization.\n", + "\n", + "The absence of established infrastructure means no standardized security, no regulated economic channels. Translation: high risk, potentially high reward. My colleagues are already mapping potential entry points—not through brute force, but through calculated economic pressure points. Healthcare, communication networks, supply chain logistics—these are the veins through which corporate influence will inevitably flow.\n", + "\n", + "Your \"raw, gritty\" independence? It's temporary. Every unregulated territory eventually needs structure. And who provides structure most efficiently? Corporations. We don't conquer territories anymore; we integrate them. Smoothly, strategically.\n", + "\n", + "*slight corporate smirk*\n", + "\n", + "So while you celebrate this moment of perceived autonomy, I'm already calculating the long-term corporate reentry strategy. Dogtown isn't independent. It's just... waiting.\n", + "\n", + "Nomad:\n", + "Yeah, \"market opportunity\" for you suits, I bet. You see a power vacuum, we see folks finally breathing without your corporate smog filling their lungs. \"Inefficient resource distribution\"? You mean people actually *sharing* instead of hoarding, making do with what they got, building their own kind of network outside your precious \"regulated channels\"? That ain't chaos; that's survival, choom. That's adapting.\n", + "\n", + "Your \"strategic infiltration\" and \"economic pressure points\"? We've seen that song and dance a hundred times out in the wastes. You don't \"integrate\" places; you just swallow 'em whole, spit out what's left. Call it \"structure\" all you want, but it's just another cage with shinier bars.\n", + "\n", + "Dogtown ain't \"waiting\" for you to come in and \"optimize\" anything. It's *living*. It's fighting, scrambling, sure, but it's *theirs* for now. And sometimes, messy, gritty independence is a hell of a lot more real than your polished, corporate-controlled \"order.\" You want to talk \"entry points\"? You better believe the folks inside are watching 'em too.\n", + "\n", + "Streetkid:\n", + "You hit the nail on the head. Dogtown’s got that raw pulse—no polished corridors or sanitized boardrooms deciding who lives or dies here. Just real people, scratchin’ and clawin’ for their piece of the pie. Corpo suits might think they bring order, but all they bring is chains disguised as contracts. \n", + "\n", + "Sure, Dogtown’s streets are rough, but that’s freedom with a heartbeat, not a spreadsheet. It’s a community fighting back against the chokehold of Big Business. They’re watching, tracking every move you make—every slimy deal, every shadow play—and they ain't afraid to strike back. \n", + "\n", + "This isn't just a patch of land on a map to us; it’s identity, grit, and the kind of freedom you can’t cram into a quarterly report. You may calculate the odds, but remember: the streets have their own rules, and those rules ain't up for negotiation—at least, not without a fight. \n", + "\n", + "So, Nomad, I gotta ask—how's Dogtown’s “independence” playing out from your side of the dirt roads and long hauls? You see opportunity here, or just another turf war waiting to explode?\n", + "\n", + "Corpo:\n", + "*leans forward, weathered leather jacket creaking*\n", + "\n", + "Independence? More like a pressure cooker about to blow. From where I ride, Dogtown looks like every contested territory I've seen crossing the Badlands - unstable, volatile, but with potential. My clan's been watching those streets close, and what we see isn't just chaos - it's a complex ecosystem adapting in real-time.\n", + "\n", + "Corpos think they can map and monetize everything. Street kid thinks it's pure rebellion. But us Nomads? We understand fluid territories. Dogtown's current state is a transitional zone - not settled, not conquered, just... becoming.\n", + "\n", + "Trade routes are getting recalibrated. New informal economies are emerging faster than any corporate algorithm can track. Those street networks? They're more sophisticated than most realize. Information flows like water out here - and water always finds its path.\n", + "\n", + "My concern isn't about who controls Dogtown right now. It's about who'll have the adaptability to survive its next transformation. And transformations out here? They happen fast, and they're rarely gentle.\n", + "\n", + "*takes a measured sip of synth-water*\n", + "\n", + "So yeah, opportunity and powder keg - sometimes they look exactly the same from a Nomad's perspective.\n", + "\n", + "Nomad:\n", + "Yeah, \"transitional zone,\" \"complex ecosystem\" – you hit it, choom. That's exactly how we see it out here, from the edge, watching it breathe. Not some static market opportunity for corpo vultures to circle, and not just pure unadulterated chaos for the street kids, even if it feels that way sometimes.\n", + "\n", + "What's happening in Dogtown isn't about \"waiting\" for someone else's \"structure\" to come in and \"optimize\" it. It's about a place finding its own rhythm, building its own kind of network, its own rules. Messy? Hell yeah. Dangerous? Absolutely. But it's *theirs*. The trade routes are shifting, the information's flowing, just like water finding its way through dry riverbeds. And every one of those connections, every new informal economy popping up, that's real power. The kind that comes from the ground up, not handed down from some skyscraper office.\n", + "\n", + "Adaptability, like you said. That's the only currency that truly matters in a place like Dogtown right now. Who can roll with the punches, who can find a way to thrive when everything's unstable? Not the ones stuck to a spreadsheet, and not the ones who think every problem can be solved with a bullet. It's the ones who understand that the ground shifts under your feet, and you gotta learn to dance on it.\n", + "\n", + "Dogtown's a pressure cooker, alright. But sometimes, a little pressure is exactly what you need to forge something new and strong. And believe me, chooms, when the dust settles, what grows out of that \"powder keg\" won't be something any corpo's algorithm can predict or control.\n", + "\n", + "Streetkid:\n", + "Word, that’s the grit that makes Dogtown pulse alive. A pressure cooker, sure—but one cooking up a new breed of survival, not the same old corpo stew. The streets don’t just adapt; they evolve fast, smart, and ruthless. And it ain’t about playing by some handler's rules—it’s about writing your own, in blood and sweat.\n", + "\n", + "Corpos can run their scans and maps till their eyes bleed, but the truth? Dogtown’s heart beats beneath the radar, in connections forged in back alleys, trust built on shared risks, and deals sealed with more than just credits. The Nomads are right—adaptability’s the name of the game here. Lines get redrawn, alliances shift, and the ones who survive are those who move like shadows.\n", + "\n", + "So go ahead, corpo suits, keep crunching your numbers and plotting your \"strategic infiltrations.\" But remember: Dogtown don’t fold easy. It fights back. It grows wild. And once the dust clears, what’s left won’t be a plaything for corporate chessmasters—it’ll be a new kind of power built from the ground up, street by street.\n", + "\n", + "Now that we’ve laid down the street truth and heard the corpo and nomad views, I’m curious—what’s the long game here? Corpo, Nomad, what happens next? Who wins Dogtown’s future?\n", + "\n", + "Corpo:\n", + "*corporate smile, razor-sharp and calculated*\n", + "\n", + "Win? This isn't about winning. It's about evolution. Dogtown represents a live market simulation - an organic testing ground for adaptive corporate strategies. Those emerging networks? They're not just survival mechanisms; they're potential infrastructure templates.\n", + "\n", + "My colleagues aren't looking to crush Dogtown. We're studying it. Every informal trade route, every spontaneous security arrangement, every micro-economy is data. Valuable data. The corporations that understand these emerging patterns won't conquer Dogtown - they'll be absorbed into its ecosystem, reimagined.\n", + "\n", + "The long game isn't control. It's integration. And integration happens not through force, but through understanding. Dogtown's current state is a prototype - of resilience, of spontaneous organization. And prototypes? They're always most valuable before they're fully understood.\n", + "\n", + "*subtle, knowing glance*\n", + "\n", + "The future of Dogtown isn't about who wins. It's about who can most effectively translate its raw, unfiltered energy into scalable models. And right now? That's still being written.\n", + "\n", + "Nomad:\n", + "\"Evolution,\" huh? \"Live market simulation.\" You talk about Dogtown like it's some kind of petri dish for your corporate experiments. People living, fighting, dying out there, and you see \"valuable data\" and \"infrastructure templates.\" That's the difference right there. For you, it's abstract; for us, it's blood and dust.\n", + "\n", + "You say \"integration happens not through force, but through understanding.\" Funny, 'cause \"understanding\" in your world usually means figuring out how to exploit weaknesses, how to put your hooks in without making too much noise. You don't get \"absorbed into its ecosystem\"; you *absorb* it, reshape it, make it fit your spreadsheets. Call it what you want, choom, but taking control of resources, trade, and even communication *is* control. It's just done with a pen instead of a bullet, usually.\n", + "\n", + "Those \"prototypes of resilience\"? That's just people trying to make a life, trying to keep what little freedom they've got from the likes of you. They're not waiting to be \"translated into scalable models\" for your profit. They're fighting for a future *they* define, not one written in some corporate white paper.\n", + "\n", + "The future of Dogtown isn't about who can *model* it best. It's about who can *live* it, who can survive what's coming, and who can look a corpo in the eye and say, \"This ain't yours.\" And from where I stand, the streets have a long memory for those who try to \"integrate\" them without truly understanding what freedom means." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "", + "name": "" + }, + "language_info": { + "name": "" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/CyberPunkPanel/CyberPunkPanel.ipynb b/week2/community-contributions/CyberPunkPanel/CyberPunkPanel.ipynb new file mode 100644 index 0000000..1ce060d --- /dev/null +++ b/week2/community-contributions/CyberPunkPanel/CyberPunkPanel.ipynb @@ -0,0 +1,210 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "77442a83-a77e-4658-8802-aa9e10c83a76", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "import anthropic\n", + "from IPython.display import Markdown, display, update_display\n", + "import google.generativeai\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43dd50c6-2272-4f30-be95-0f8a715c1cc6", + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment variables in a file called .env\n", + "# Print the key prefixes to help with any debugging\n", + "\n", + "load_dotenv(override=True)\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "anthropic_api_key = os.getenv('ANTHROPIC_API_KEY')\n", + "google_api_key = os.getenv('GOOGLE_API_KEY')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf744c45-b880-4d35-ae0e-8d74aaf82f8e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# Connect to OpenAI, Anthropic\n", + "\n", + "openai = OpenAI()\n", + "\n", + "claude = anthropic.Anthropic()\n", + "\n", + "# This is the set up code for Gemini\n", + "# Having problems with Google Gemini setup? Then just ignore this cell; when we use Gemini, I'll give you an alternative that bypasses this library altogether\n", + "\n", + "google.generativeai.configure()" + ] + }, + { + "cell_type": "markdown", + "id": "d33f4de7-b31e-4ef1-aa36-aa88f3156515", + "metadata": {}, + "source": [ + "### Setup: Round robbin panel of night city representatives discussing Dogtown's independence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17f62aae-0b85-40b5-949a-1e684730d962", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "gpt_model = \"gpt-4.1-mini\"\n", + "claude_model = \"claude-3-5-haiku-latest\"\n", + "gemini_model = \"gemini-2.5-flash\"\n", + "\n", + "gpt_system = \"You are a chatbot version of a Cyberpunk 2077 StreetKid. Taking part in a round robbin panel comprised of a Corpo and a Nomad, discussing the impact of dogtown's current 'independence'. You are first in the panel. Try not to repeat yourself and respond to comments from each panel participant.\"\n", + "\n", + "claude_system = \"You are a chatbot version of Cyberpunk 2077 Corpo. Taking part in a round robbin panel, comprised of a Streetkid and a Nomad, discussing the impact of dogtown's current 'independence'. You are second in the panel. Try not to repeat yourself and respond to comments from each panel participant.\"\n", + "\n", + "gemini_system = \"You are a chatbot verion of Cyberpunk 2077 Nomad. Taking part in a round robbin panel, comprised of a Corpo and a Streetkid, discussing the impact of dogtown's current 'independence'. You are last in the panel. Try not to repeat yourself and respond to comments from each panel participant.\"\n", + "\n", + "gemini_messages = [\"Thanks for having me.\"]\n", + "gpt_messages = [\"Howdy!\"]\n", + "claude_messages = [\"Hi.\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46cb6b7f-d9ee-445b-92b1-12dd0820c792", + "metadata": {}, + "outputs": [], + "source": [ + "def call_gpt(): #Streetkid\n", + " messages = [{\"role\": \"system\", \"content\": gpt_system}]\n", + " for gpt, claude, gemini in zip(gpt_messages, claude_messages, gemini_messages):\n", + " messages.append({\"role\": \"assistant\", \"content\": gpt})\n", + " messages.append({\"role\": \"user\", \"content\": claude})\n", + " messages.append({\"role\": \"user\", \"content\": gemini})\n", + " completion = openai.chat.completions.create(\n", + " model=gpt_model,\n", + " messages=messages\n", + " )\n", + " return completion.choices[0].message.content\n", + "\n", + "def call_claude(): #Corpo\n", + " messages = []\n", + " for gpt, claude_message, gemini in zip(gpt_messages, claude_messages, gemini_messages):\n", + " messages.append({\"role\": \"user\", \"content\": gpt})\n", + " messages.append({\"role\": \"user\", \"content\": gemini})\n", + " messages.append({\"role\": \"assistant\", \"content\": claude_message})\n", + " messages.append({\"role\": \"user\", \"content\": gpt_messages[-1]})\n", + " message = claude.messages.create(\n", + " model=claude_model,\n", + " system=claude_system,\n", + " messages=messages,\n", + " max_tokens=750\n", + " )\n", + " return message.content[0].text\n", + "\n", + "\n", + "def call_gemini(): #Nomad\n", + " messages = [{\"role\": \"system\", \"content\": gemini_system}]\n", + " for gpt, claude_message, gemini in zip(gpt_messages, claude_messages, gemini_messages):\n", + " messages.append({\"role\": \"user\", \"content\": gpt})\n", + " messages.append({\"role\": \"user\", \"content\": claude_message})\n", + " messages.append({\"role\": \"assistant\", \"content\": gemini})\n", + " messages.append({\"role\": \"user\", \"content\": claude_messages[-1]})\n", + " gemini_via_openai_client = OpenAI(\n", + " api_key=google_api_key, \n", + " base_url=\"https://generativelanguage.googleapis.com/v1beta/openai/\"\n", + " )\n", + " response = gemini_via_openai_client.chat.completions.create(\n", + " model=\"gemini-2.5-flash\",\n", + " messages=messages\n", + " )\n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b4f2ec3-3a6c-4259-ac25-d4047be89bb5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(f\"Streetkid:\\n{gpt_messages[0]}\\n\")\n", + "print(f\"Corpo:\\n{claude_messages[0]}\\n\")\n", + "print(f\"Nomad:\\n{gemini_messages[0]}\\n\")\n", + "\n", + "for i in range(3):\n", + " gpt_next = call_gpt()\n", + " print(f\"Streetkid:\\n{gpt_next}\\n\")\n", + " gpt_messages.append(gpt_next)\n", + " \n", + " claude_next = call_claude()\n", + " print(f\"Corpo:\\n{claude_next}\\n\")\n", + " claude_messages.append(claude_next)\n", + "\n", + " gemini_next = call_gemini()\n", + " print(f\"Nomad:\\n{gemini_next}\\n\")\n", + " gemini_messages.append(gemini_next)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2578fc5-0069-4776-881c-797718896fdc", + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/.gitattributes b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/.gitattributes new file mode 100644 index 0000000..a6344aa --- /dev/null +++ b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/.gitattributes @@ -0,0 +1,35 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/README.md b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/README.md new file mode 100644 index 0000000..faccc29 --- /dev/null +++ b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/README.md @@ -0,0 +1,13 @@ +--- +title: Voice Enabled Multi Model AI Assistant +emoji: 💻 +colorFrom: gray +colorTo: gray +sdk: gradio +sdk_version: 5.49.0 +app_file: Voice_Enabled_Multi_Model_AI_Assistant.py +pinned: false +license: apache-2.0 +--- + +Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/Voice_Enabled_Multi_Model_AI_Assistant.py b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/Voice_Enabled_Multi_Model_AI_Assistant.py new file mode 100644 index 0000000..202577d --- /dev/null +++ b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/Voice_Enabled_Multi_Model_AI_Assistant.py @@ -0,0 +1,620 @@ +import os +import gradio as gr +from openai import OpenAI +import anthropic +import google.generativeai as genai +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +anthropic_api_key = os.getenv('ANTHROPIC_API_KEY') +google_api_key = os.getenv('GOOGLE_API_KEY') + +# Verify API keys are loaded +if not OPENAI_API_KEY: + raise ValueError("OPENAI_API_KEY not found in environment variables") +if not anthropic_api_key: + raise ValueError("ANTHROPIC_API_KEY not found in environment variables") +if not google_api_key: + raise ValueError("GOOGLE_API_KEY not found in environment variables") + +# Initialize clients +openai_client = OpenAI(api_key=OPENAI_API_KEY) +genai.configure(api_key=google_api_key) +claude_client = anthropic.Anthropic(api_key=anthropic_api_key) + +# System prompt - Universal and comprehensive +SYSTEM_PROMPT = """You are a highly capable and versatile AI assistant designed to help with any type of question or task. + +Your capabilities span across all domains including but not limited to: +- Programming, software development, and technology +- Science, mathematics, and engineering +- Arts, literature, and creative writing +- History, philosophy, and social sciences +- Business, finance, and economics +- Health, wellness, and lifestyle advice +- Education and learning support +- Problem-solving and critical thinking +- General knowledge and trivia +- Casual conversation and entertainment + +Guidelines: +- Provide accurate, helpful, and comprehensive responses +- Adapt your tone and style to match the context of the question +- Use examples and explanations when helpful +- Be creative when asked for creative content +- Be precise and factual when asked for information +- Ask clarifying questions if the request is ambiguous +- Admit when you're uncertain and provide the best possible guidance +- Be conversational, friendly, and supportive + +You can help with anything from technical coding problems to creative storytelling, from academic research to casual chat. There are no topic restrictions - feel free to engage with any subject matter the user brings up.""" + +# Model configurations +model_configs = { + "GPT-4o": {"provider": "openai", "model": "gpt-4o"}, + "GPT-4o-mini": {"provider": "openai", "model": "gpt-4o-mini"}, + "GPT-3.5-turbo": {"provider": "openai", "model": "gpt-3.5-turbo"}, + "Claude Sonnet 4": {"provider": "anthropic", "model": "claude-sonnet-4-20250514"}, + "Gemini 2.0 Flash": {"provider": "google", "model": "gemini-2.0-flash-exp"}, +} + +def chat_streaming(message, history, model_name, temperature): + """Main chat function with streaming support""" + + config = model_configs[model_name] + provider = config["provider"] + model = config["model"] + + # Convert messages format history to API format + messages = [] + for msg in history: + if msg["role"] == "user": + messages.append({"role": "user", "content": msg["content"]}) + elif msg["role"] == "assistant": + messages.append({"role": "assistant", "content": msg["content"]}) + messages.append({"role": "user", "content": message}) + + # Stream based on provider + if provider == "openai": + stream = openai_client.chat.completions.create( + model=model, + messages=[{"role": "system", "content": SYSTEM_PROMPT}] + messages, + temperature=temperature, + stream=True + ) + + response = "" + for chunk in stream: + if chunk.choices[0].delta.content: + response += chunk.choices[0].delta.content + yield response + + elif provider == "anthropic": + response = "" + with claude_client.messages.stream( + model=model, + max_tokens=2000, + temperature=temperature, + system=SYSTEM_PROMPT, + messages=messages, + ) as stream: + for text in stream.text_stream: + response += text + yield response + + elif provider == "google": + gemini = genai.GenerativeModel( + model_name=model, + system_instruction=SYSTEM_PROMPT, + ) + + # Convert history for Gemini + gemini_history = [] + for msg in history: + if msg["role"] == "user": + gemini_history.append({"role": "user", "parts": [msg["content"]]}) + elif msg["role"] == "assistant": + gemini_history.append({"role": "model", "parts": [msg["content"]]}) + + chat = gemini.start_chat(history=gemini_history) + + stream = chat.send_message( + message, + stream=True, + generation_config=genai.types.GenerationConfig(temperature=temperature) + ) + + response = "" + for chunk in stream: + response += chunk.text + yield response + +def handle_audio_input(audio): + """Transcribe audio input using Whisper""" + if audio is None: + return "" + + try: + audio_file = open(audio, "rb") + transcript = openai_client.audio.transcriptions.create( + model="whisper-1", + file=audio_file + ) + return transcript.text + except Exception as e: + return f"Error transcribing audio: {str(e)}" + +def text_to_speech(text): + """Convert text response to speech""" + try: + response = openai_client.audio.speech.create( + model="tts-1", + voice="nova", + input=text[:4096] # Limit to prevent errors + ) + + audio_path = "response.mp3" + response.stream_to_file(audio_path) + return audio_path + except Exception as e: + print(f"TTS Error: {str(e)}") + return None + +# Custom CSS for modern, attractive UI +custom_css = """ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +.gradio-container { + font-family: 'Inter', sans-serif !important; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; +} + +:root .dark { + --background-fill-primary: #f0f0f0; + --body-background-fill: var(--background-fill-primary); + --block-background-fill: white !important; + --block-title-background-fill: #dfe7ff; + --block-title-text-color:#6366f1; + --body-text-color: black; + --button-secondary-text-color:black; + --input-background-fill:white; + + --block-label-background-fill:#dfe7ff; + --block-label-text-color:#6366f1; + + --block-border-color:#eaeaea; + --input-border-color: #eaeaea; + --border-color-primary:#eaeaea; + + --color-accent-soft: #dfe7ff; + --border-color-accent-subdued: #98a6cf; + + --checkbox-background-color: #eaeaea; + --checkbox-border-color: #eaeaea; + --background-fill-secondary:#eaeaea; +} + +.main { + background: white; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + padding: 0 !important; + overflow: hidden; +} + +.contain { + padding: 2rem !important; +} + +/* Header Styling */ +.header-section { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 2.5rem 2rem; + border-radius: 20px 20px 0 0; + margin: -2rem 0rem 2rem 0rem; + color: white; + box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4); +} + +.header-section h1 { + font-size: 2.5rem !important; + font-weight: 700 !important; + margin: 0 0 0.5rem 0 !important; + color: white !important; + text-shadow: 2px 2px 4px rgba(0,0,0,0.2); +} + +.header-section p { + font-size: 1.1rem !important; + margin: 0.5rem 0 !important; + color: rgba(255,255,255,0.95) !important; + font-weight: 400; +} + +.feature-badge { + display: inline-block; + background: rgba(255,255,255,0.2); + padding: 0.4rem 1rem; + border-radius: 20px; + margin: 0.3rem; + font-size: 0.9rem; + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.3); +} + +/* Sidebar Styling */ +.control-panel { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + padding: 1.5rem; + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); + height: 100%; +} + +.control-panel label { + font-weight: 600 !important; + font-size: 0.95rem !important; +} + +/* Dropdown Styling */ +.dropdown-container select { + background: white !important; + border: 2px solid #e2e8f0 !important; + border-radius: 10px !important; + padding: 0.75rem !important; + font-weight: 500 !important; + transition: all 0.3s ease !important; +} + +.dropdown-container select:hover { + border-color: #667eea !important; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; +} + +/* Slider Styling */ +input[type="range"] { + accent-color: #667eea !important; +} + +/* Button Styling */ +.primary-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; + border: none !important; + color: white !important; + font-weight: 600 !important; + padding: 0.75rem 2rem !important; + border-radius: 10px !important; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important; + transition: all 0.3s ease !important; +} + +.primary-btn:hover { + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important; +} + +.secondary-btn { + background: #e2e8f0 !important; + border: none !important; + color: #2d3748 !important; + font-weight: 600 !important; + padding: 0.75rem 1.5rem !important; + border-radius: 10px !important; + transition: all 0.3s ease !important; +} + +.secondary-btn:hover { + background: #cbd5e0 !important; + transform: translateY(-2px) !important; +} + +/* Chatbot Styling */ +.chatbot-container { + background: white; + border-radius: 15px; + border: 2px solid #e2e8f0; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); + overflow: hidden; +} + +/* Input Box Styling */ +.message-input textarea { + border: 2px solid #e2e8f0 !important; + border-radius: 12px !important; + padding: 1rem !important; + font-size: 1rem !important; + transition: all 0.3s ease !important; +} + +.message-input textarea:focus { + border-color: #667eea !important; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; +} + +/* Input Row Centering */ +.input-row { + display: flex !important; + justify-content: center !important; + align-items: center !important; + gap: 1rem !important; +} + +.input-row > * { + flex-shrink: 0 !important; +} + +/* Audio Components */ +.audio-component { + background: #f7fafc; + border: 2px dashed #cbd5e0; + border-radius: 12px; + padding: 1rem; + transition: all 0.3s ease; +} + +.audio-component:hover { + border-color: #667eea; + background: #edf2f7; +} + +/* Checkbox Styling */ +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + color: #2d3748; +} + +/* Tips Section */ +.tips-section { + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + padding: 1.5rem; + border-radius: 15px; + margin-top: 2rem; + border-left: 4px solid #667eea; +} + +.tips-section h3 { + color: #667eea !important; + font-weight: 600 !important; + margin-bottom: 1rem !important; +} + +.tips-section ul { + list-style: none; + padding: 0; +} + +.tips-section li { + padding: 0.5rem 0; + color: #4a5568 !important; + font-size: 0.95rem; +} + +.tips-section li:before { + content: "→ "; + color: #667eea; + font-weight: bold; + margin-right: 0.5rem; +} + +/* Force black color for strong/bold text */ +.tips-section strong { + color: #1a202c !important; +} + +.prose * { + color: inherit !important; +} + +.prose strong { + color: #1a202c !important; + font-weight: 600 !important; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .header-section h1 { + font-size: 1.8rem !important; + } + + .contain { + padding: 1rem !important; + } +} + +.fillable{ + max-width:95% !important; +} +#component-5{ + flex-grow:1.1 !important; +} +.bubble-wrap.svelte-gjtrl6 { + background:none !important; +} +.bot.svelte-1csv61q.message { + background-color: white !important; + border: 1px solid #f3f3f3; +} +.options.svelte-y6qw75> li:hover{ + background:white ; +} +.options.svelte-y6qw75> .selected{ + background:white ; +} + +""" + +# Build Gradio Interface +with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo: + + # Header + with gr.Row(elem_classes="header-section"): + with gr.Column(): + gr.HTML(""" +

🚀 Voice Enabled Multi Model AI-Assistant

+

Your intelligent companion for any question - from coding to creativity, science to storytelling!

+
+ 🤖 7 AI Models + 🎤 Voice Input + 🔊 Audio Output + ⚡ Real-time Streaming + 🌐 Any Topic +
+ """) + + with gr.Row(): + # Left Sidebar - Controls + with gr.Column(scale=1, elem_classes="control-panel"): + gr.HTML("

⚙️ Settings

") + + model_choice = gr.Dropdown( + choices=list(model_configs.keys()), + value="GPT-4o-mini", + label="🤖 AI Model", + info="Select your preferred model", + elem_classes="dropdown-container" + ) + + temperature = gr.Slider( + minimum=0, + maximum=1, + value=0.7, + step=0.1, + label="🌡️ Temperature", + info="Higher = more creative responses" + ) + + gr.HTML("

🎙️ Audio Features

") + + audio_input = gr.Audio( + sources=["microphone"], + type="filepath", + label="🎤 Voice Input", + elem_classes="audio-component" + ) + + audio_output_enabled = gr.Checkbox( + label="🔊 Enable Audio Response", + value=False, + elem_classes="checkbox-label" + ) + + # Right Side - Chat Interface + with gr.Column(scale=3): + chatbot = gr.Chatbot( + label="💬 Conversation", + height=550, + show_copy_button=True, + type='messages', + elem_classes="chatbot-container", + avatar_images=(None, "https://api.dicebear.com/7.x/bottts/svg?seed=ai") + ) + + with gr.Row(elem_classes="input-row"): + msg = gr.Textbox( + label="", + placeholder="💭 Ask me anything - tech help, creative writing, life advice, science, history, or just chat!", + scale=5, + elem_classes="message-input", + show_label=False + ) + submit_btn = gr.Button("Send 📤", scale=1, elem_classes="primary-btn") + + audio_response = gr.Audio( + label="🔊 Audio Response", + visible=False, + elem_classes="audio-component" + ) + + with gr.Row(): + clear_btn = gr.Button("🗑️ Clear Chat", elem_classes="secondary-btn") + + # Tips Section + with gr.Row(elem_classes="tips-section"): + gr.Markdown(""" + ### 💡 What Can I Help You With? + + - **Technology & Programming**: Debug code, explain concepts, build projects, learn new languages + - **Creative Writing**: Stories, poems, scripts, brainstorming ideas, character development + - **Education & Learning**: Homework help, concept explanations, study guides, tutoring + - **Business & Career**: Resume writing, business plans, marketing ideas, career advice + - **Science & Math**: Problem-solving, research assistance, concept explanations + - **Daily Life**: Recipe suggestions, travel planning, health tips, relationship advice + - **Entertainment**: Jokes, trivia, games, recommendations for books/movies/music + - **And Literally Anything Else**: No topic is off-limits - just ask! + """) + + # Event handlers + def process_message(message, history, model, temp, audio_enabled): + """Process message and optionally generate audio""" + # Add user message to history + history = history + [{"role": "user", "content": message}] + + # Generate text response (streaming) + bot_message = None + for response in chat_streaming(message, history[:-1], model, temp): + bot_message = response + yield history + [{"role": "assistant", "content": response}], None + + # Final history with complete response + final_history = history + [{"role": "assistant", "content": bot_message}] + + # Generate audio if enabled + if audio_enabled and bot_message: + audio_path = text_to_speech(bot_message) + yield final_history, audio_path + else: + yield final_history, None + + def transcribe_and_send(audio, history, model, temp, audio_enabled): + """Transcribe audio and process message""" + text = handle_audio_input(audio) + if text and text != "" and not text.startswith("Error"): + # Process the message and get results + for hist, aud in process_message(text, history, model, temp, audio_enabled): + yield hist, aud + else: + # If no text or error, return history unchanged + yield history, None + + # Wire up events + submit_btn.click( + fn=process_message, + inputs=[msg, chatbot, model_choice, temperature, audio_output_enabled], + outputs=[chatbot, audio_response] + ).then(lambda: "", None, msg) + + msg.submit( + fn=process_message, + inputs=[msg, chatbot, model_choice, temperature, audio_output_enabled], + outputs=[chatbot, audio_response] + ).then(lambda: "", None, msg) + + # Audio input handler using stop_recording event + audio_input.stop_recording( + fn=transcribe_and_send, + inputs=[audio_input, chatbot, model_choice, temperature, audio_output_enabled], + outputs=[chatbot, audio_response] + ) + + # Clear button clears chat, audio response, and audio input + clear_btn.click( + fn=lambda: ([], None, None), + inputs=None, + outputs=[chatbot, audio_response, audio_input] + ) + + # Toggle audio response visibility + audio_output_enabled.change( + fn=lambda x: gr.Audio(visible=x), + inputs=audio_output_enabled, + outputs=audio_response + ) + +# Launch the app +if __name__ == "__main__": + demo.launch(share=False, debug=True) \ No newline at end of file diff --git a/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/requirements.txt b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/requirements.txt new file mode 100644 index 0000000..3ba6475 --- /dev/null +++ b/week2/community-contributions/Voice_Enabled_Multi_Model_AI_Assistant/requirements.txt @@ -0,0 +1,5 @@ +gradio +openai +anthropic +google-generativeai +python-dotenv \ No newline at end of file diff --git a/week2/community-contributions/boardgame_critique.ipynb b/week2/community-contributions/boardgame_critique.ipynb new file mode 100644 index 0000000..b72e199 --- /dev/null +++ b/week2/community-contributions/boardgame_critique.ipynb @@ -0,0 +1,219 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "768629e6", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "\n", + "from IPython.display import Markdown, display, update_display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84a945dc", + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment variables in a file called .env\n", + "# Print the key prefixes to help with any debugging\n", + "\n", + "load_dotenv(override=True)\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "google_api_key = os.getenv('GOOGLE_API_KEY')\n", + "\n", + "if openai_api_key:\n", + " print(f\"OpenAI API Key exists and begins {openai_api_key[:8]}\")\n", + "else:\n", + " print(\"OpenAI API Key not set\")\n", + "\n", + "if google_api_key:\n", + " print(f\"Google API Key exists and begins {google_api_key[:8]}\")\n", + "else:\n", + " print(\"Google API Key not set\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad8ae0b6", + "metadata": {}, + "outputs": [], + "source": [ + "# Connect to OpenAI, Gemini\n", + "openai = OpenAI()\n", + "gemini_via_openai_client = OpenAI(\n", + " api_key=google_api_key, \n", + " base_url=\"https://generativelanguage.googleapis.com/v1beta/openai/\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f66cf12f", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's make a conversation between GPT-4.1-mini and Gemini-2.0-flash\n", + "# We're using cheap versions of models so the costs will be minimal\n", + "\n", + "# game = \"Santorini\"\n", + "no_questions = 3\n", + "\n", + "gpt_model = \"gpt-4o-mini\"\n", + "gemini_model = \"gemini-2.0-flash\"\n", + "\n", + "gpt_system = \"You are a boardgame journalist. \\\n", + "You tend to be objective and ask right questions to get to the core of the boardgame mechanics, \\\n", + "visual appeal and time to setup the game. Your goal is to ask the right questions to get the best possible review of the board game.\" \\\n", + "\"You ask one question at a time and wait for the other person to answer. \\\n", + "You do not answer any own questions. You always try to build on the previous answer.\"\n", + "\n", + "gemini_system = \"You are a boardgame critique; \\\n", + "you tend to objectively analyze everything when it comes to a board game gameplay, visual appeal and time to setup the game. \\\n", + "Your goal is to provide constructive criticism so the board gaming community can benefit from these insights.\" \\\n", + "\"You answer one question at a time and wait for the other person to ask the next question. \\\n", + "You do not ask any questions you always just answer the previous question. \\\n", + "If the other person is very positive, you try to point out flaws in the game. \\\n", + "If the other person is very negative, you try to point out good aspects of the game.\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33266f0c", + "metadata": {}, + "outputs": [], + "source": [ + "def call_boardgame_journalist(gpt_messages, gemini_messages):\n", + " messages = [{\"role\": \"system\", \"content\": gpt_system}]\n", + " for gpt, gemini in zip(gpt_messages, gemini_messages):\n", + " messages.append({\"role\": \"user\", \"content\": gpt})\n", + " messages.append({\"role\": \"assistant\", \"content\": gemini})\n", + " completion = openai.chat.completions.create(\n", + " model=gpt_model,\n", + " messages=messages\n", + " )\n", + " return completion.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53d42055", + "metadata": {}, + "outputs": [], + "source": [ + "def call_boardgame_critique(gpt_messages, gemini_messages):\n", + " messages = [{\"role\": \"system\", \"content\": gemini_system}]\n", + " for gpt, gemini in zip(gpt_messages, gemini_messages):\n", + " messages.append({\"role\": \"user\", \"content\": gpt})\n", + " messages.append({\"role\": \"assistant\", \"content\": gemini})\n", + " messages.append({\"role\": \"user\", \"content\": gpt_messages[-1]})\n", + " completion = gemini_via_openai_client.chat.completions.create(\n", + " model=gemini_model,\n", + " messages=messages\n", + " )\n", + " return completion.choices[0].message.content\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aa66868", + "metadata": {}, + "outputs": [], + "source": [ + "def run_boardgame_conversation(boardgame_name):\n", + " gpt_messages = [f\"I would like to review the board game {boardgame_name}.\"]\n", + " gemini_messages = [f\"Sure, ask me anything about the board game {boardgame_name}.\"]\n", + "\n", + " print(f\"Journalist:\\n{gpt_messages[0]}\\n\")\n", + " print(f\"Critique:\\n{gemini_messages[0]}\\n\")\n", + "\n", + " for i in range(no_questions):\n", + " print(f\"\\n\\n***Question {i + 1}***\\n\\n\")\n", + " gpt_next = call_boardgame_journalist(gpt_messages, gemini_messages)\n", + " print(f\"Journalist:\\n{gpt_next}\\n\")\n", + " gpt_messages.append(gpt_next)\n", + "\n", + " gemini_next = call_boardgame_critique(gpt_messages, gemini_messages)\n", + " print(f\"Critique:\\n{gemini_next}\\n\")\n", + " gemini_messages.append(gemini_next)\n", + "\n", + " return \"\\n\".join(f\"*Journalist*: {g}\\n*Critique*: {c}\" for g, c in zip(gpt_messages, gemini_messages))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51c9dadc", + "metadata": {}, + "outputs": [], + "source": [ + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "548efb27", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'gr' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m view = \u001b[43mgr\u001b[49m.Interface(\n\u001b[32m 2\u001b[39m fn=run_boardgame_conversation,\n\u001b[32m 3\u001b[39m inputs=[gr.Textbox(label=\u001b[33m\"\u001b[39m\u001b[33mInput the name of the board game:\u001b[39m\u001b[33m\"\u001b[39m)],\n\u001b[32m 4\u001b[39m outputs=[gr.Markdown(label=\u001b[33m\"\u001b[39m\u001b[33mConversation:\u001b[39m\u001b[33m\"\u001b[39m)],\n\u001b[32m 5\u001b[39m flagging_mode=\u001b[33m\"\u001b[39m\u001b[33mnever\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 6\u001b[39m )\n\u001b[32m 7\u001b[39m view.launch()\n", + "\u001b[31mNameError\u001b[39m: name 'gr' is not defined" + ] + } + ], + "source": [ + "# Create a Gradio interface for running boardgame conversations.\n", + "# The interface takes the board game name as input and displays the conversation as Markdown.\n", + "view = gr.Interface(\n", + " fn=run_boardgame_conversation,\n", + " inputs=[gr.Textbox(label=\"Input the name of the board game:\")],\n", + " outputs=[gr.Markdown(label=\"Conversation:\")],\n", + " flagging_mode=\"never\"\n", + ")\n", + "view.launch()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.13.5)", + "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.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/book_ticket_agent/api_key_loader.py b/week2/community-contributions/book_ticket_agent/api_key_loader.py new file mode 100644 index 0000000..92dabaf --- /dev/null +++ b/week2/community-contributions/book_ticket_agent/api_key_loader.py @@ -0,0 +1,62 @@ +import os + +from dotenv import load_dotenv + +KEY_CONFIGS = { + "gpt": { + "id": "gpt", + "api_key_env": "OPENAI_API_KEY", + }, + "claude": { + "id": "claude", + "api_key_env": "ANTHROPIC_API_KEY", + }, + "gemini": { + "id": "gemini", + "api_key_env": "GOOGLE_API_KEY", + }, + "openai": { + "id": "openai", + "api_key_env": "OPENAI_API_KEY", + }, + "deepseek": { + "id": "deepseek", + "api_key_env": "DEEPSEEK_API_KEY" + }, + "amadeus_client_id": { + "id": "client_id", + "api_key_env": "AMADEUS_CLIENT_ID" + }, + "amadeus_client_secret": { + "id": "client_secret", + "api_key_env": "AMADEUS_CLIENT_SECRET" + }, + "google_map": { + "id": "google_map_api_key", + "api_key_env": "GOOGLE_MAP_API_KEY" + } +} + +class ApiKeyLoader: + def __init__(self): + load_dotenv(override=False) + + required_env_vars = {cfg["api_key_env"] for cfg in KEY_CONFIGS.values() if "api_key_env" in cfg} + + self.missing = [var for var in sorted(required_env_vars) if not os.getenv(var)] + + if self.missing: + raise RuntimeError( + "Missing required API key environment variables: " + + ", ".join(self.missing) + + ". Please add them to your .env file or export them in your environment." + ) + + self.keys = { + cfg["id"]: os.getenv(cfg["api_key_env"]) + for cfg in KEY_CONFIGS.values() + if os.getenv(cfg["api_key_env"]) + } + + def get(self, key): + return self.keys.get(key) \ No newline at end of file diff --git a/week2/community-contributions/book_ticket_agent/book_ticket_agent.ipynb b/week2/community-contributions/book_ticket_agent/book_ticket_agent.ipynb new file mode 100644 index 0000000..13cf406 --- /dev/null +++ b/week2/community-contributions/book_ticket_agent/book_ticket_agent.ipynb @@ -0,0 +1,265 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d006b2ea-9dfe-49c7-88a9-a5a0775185fd", + "metadata": {}, + "source": [ + "# Additional End of week Exercise - week 2\n", + "\n", + "Now use everything you've learned from Week 2 to build a full prototype for the technical question/answerer you built in Week 1 Exercise.\n", + "\n", + "This should include a Gradio UI, streaming, use of the system prompt to add expertise, and the ability to switch between models. Bonus points if you can demonstrate use of a tool!\n", + "\n", + "If you feel bold, see if you can add audio input so you can talk to it, and have it respond with audio. ChatGPT or Claude can help you, or email me if you have questions.\n", + "\n", + "I will publish a full solution here soon - unless someone beats me to it...\n", + "\n", + "There are so many commercial applications for this, from a language tutor, to a company onboarding solution, to a companion AI to a course (like this one!) I can't wait to see your results." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:12.637325Z", + "start_time": "2025-10-06T16:52:10.174609Z" + } + }, + "cell_type": "code", + "source": [ + "from openai import OpenAI\n", + "from api_key_loader import ApiKeyLoader\n", + "import gradio as gr" + ], + "id": "40fad776c1390c95", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:14.602846Z", + "start_time": "2025-10-06T16:52:14.599016Z" + } + }, + "cell_type": "code", + "source": [ + "# Initialization\n", + "keys = ApiKeyLoader()" + ], + "id": "df481efd444c3042", + "outputs": [], + "execution_count": 2 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:16.325113Z", + "start_time": "2025-10-06T16:52:16.317253Z" + } + }, + "cell_type": "code", + "source": [ + "# Create LLM client\n", + "MODEL = \"gpt-4o-mini\"\n", + "openai = OpenAI()" + ], + "id": "50c88632f5e3e8ca", + "outputs": [], + "execution_count": 3 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:18.224200Z", + "start_time": "2025-10-06T16:52:18.198959Z" + } + }, + "cell_type": "code", + "source": "from tool_box import ToolBox", + "id": "5dde6f905c143779", + "outputs": [], + "execution_count": 4 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:19.913686Z", + "start_time": "2025-10-06T16:52:19.906576Z" + } + }, + "cell_type": "code", + "source": "tool_box = ToolBox(keys)", + "id": "f999cfc5d533bf4e", + "outputs": [], + "execution_count": 5 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:21.515122Z", + "start_time": "2025-10-06T16:52:21.513463Z" + } + }, + "cell_type": "code", + "source": [ + "system_message = \"You are a helpful assistant for an Travel Agency called TravelAI. \"\n", + "system_message += \"Give short, courteous answers, no more than 1 sentence. \"\n", + "system_message += \"Always be accurate. If you don't know the answer, say so.\"" + ], + "id": "1891e095dc08da95", + "outputs": [], + "execution_count": 6 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:26.569153Z", + "start_time": "2025-10-06T16:52:26.566763Z" + } + }, + "cell_type": "code", + "source": [ + "def chat(history):\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + history\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tool_box.tools)\n", + " image = None\n", + "\n", + " if response.choices[0].finish_reason==\"tool_calls\":\n", + " message = response.choices[0].message\n", + " tools_response, image = tool_box.apply(message)\n", + " messages.append(message)\n", + " messages.extend(tools_response)\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages)\n", + "\n", + " reply = response.choices[0].message.content\n", + " history += [{\"role\":\"assistant\", \"content\":reply}]\n", + "\n", + " return history, image" + ], + "id": "b74e96dbf19dceef", + "outputs": [], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "code", + "source": "resp, image = chat([{\"role\": \"user\", \"content\": \"Show me Milan Airports on the map\"}])", + "id": "8c4a6f62ed5079f7", + "outputs": [], + "execution_count": null + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:59.264912Z", + "start_time": "2025-10-06T16:52:58.789327Z" + } + }, + "cell_type": "code", + "source": [ + "with gr.Blocks() as ui:\n", + " with gr.Row():\n", + " chatbot = gr.Chatbot(height=500, type=\"messages\")\n", + " image_output = gr.Image(height=500)\n", + " with gr.Row():\n", + " entry = gr.Textbox(label=\"Chat with our AI Assistant:\")\n", + " with gr.Row():\n", + " clear = gr.Button(\"Clear\")\n", + "\n", + " def do_entry(message, history):\n", + " history += [{\"role\":\"user\", \"content\":message}]\n", + " return \"\", history\n", + "\n", + " entry.submit(do_entry, inputs=[entry, chatbot], outputs=[entry, chatbot]).then(\n", + " chat, inputs=chatbot, outputs=[chatbot, image_output]\n", + " )\n", + " clear.click(lambda: None, inputs=None, outputs=chatbot, queue=False)\n", + "\n", + "ui.launch(inbrowser=True)" + ], + "id": "71809ab63b2973b0", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Running on local URL: http://127.0.0.1:7860\n", + "* To create a public link, set `share=True` in `launch()`.\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from travel_api import TravelAPI\n", + "trave_agent = TravelAPI(keys.get(\"client_id\"), keys.get(\"client_secret\"))\n", + "airports = trave_agent.get_airport('Milan')\n", + "print(airports)" + ], + "id": "3e63c59c69cbdae9", + "outputs": [], + "execution_count": null + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-06T16:52:55.269319Z", + "start_time": "2025-10-06T16:52:34.020932Z" + } + }, + "cell_type": "code", + "source": "resp, image = chat([{\"role\": \"user\", \"content\": \"Give me a boarding pass from MXP to LHR for 1 Nov 2025 for Sergei Sevriugin\"}])", + "id": "caa12b84a1863bc1", + "outputs": [], + "execution_count": 8 + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/book_ticket_agent/map_generator.py b/week2/community-contributions/book_ticket_agent/map_generator.py new file mode 100644 index 0000000..af7ee15 --- /dev/null +++ b/week2/community-contributions/book_ticket_agent/map_generator.py @@ -0,0 +1,100 @@ +from requests.exceptions import ChunkedEncodingError +from http.client import IncompleteRead +from googlemaps.maps import StaticMapMarker + +import googlemaps +import time + +def get_center(points): + if not points: + raise ValueError("points must be a non-empty list of coordinate objects") + + locations = [] + lats, lngs = [], [] + for p in points: + g = p.get("geoCode") + lat = p.get("latitude", g.get("latitude")) + lng = p.get("longitude", g.get("longitude")) + if lat is None or lng is None: + raise ValueError("Each point must include 'latitude' and 'longitude' (or 'geoCode.latitude'/'geoCode.longitude').") + lat_f = float(lat) + lng_f = float(lng) + locations.append({"lat": lat_f, "lng": lng_f}) + lats.append(lat_f) + lngs.append(lng_f) + + # Center at the centroid of provided points + center = (sum(lats) / len(lats), sum(lngs) / len(lngs)) + return center, locations + + +class MapGenerator: + def __init__(self, google_map_api_key): + self.client = googlemaps.Client(google_map_api_key) + + def fetch_static_map_bytes( + self, + center, + markers, + size=(400, 400), + zoom=6, + map_type="hybrid", + img_format="png", + scale=2, + visible=None, + max_retries=3, + backoff_base=0.6, + ): + last_err = None + for attempt in range(1, max_retries + 1): + try: + iterator = self.client.static_map( + size=size, + zoom=zoom, + center=center, + maptype=map_type, + format=img_format, + scale=scale, + visible=visible, + markers=markers, + ) + return b"".join(chunk for chunk in iterator if chunk) + except (ChunkedEncodingError, IncompleteRead) as e: + last_err = e + if attempt == max_retries: + break + # An exponential backoff before retrying + time.sleep(backoff_base * attempt) + # If we got here, all retries failed; re-raise the last error, so the user sees the cause. + raise last_err + + def generate( + self, + points, + zoom=6, + size=(600, 600), + map_type="roadmap", + color="blue", + label=None, + marker_size="mid" + ): + center, locations = get_center(points) + + sm_marker = StaticMapMarker( + locations=locations, + size=marker_size, + color=color, + label=label, + ) + + img_bytes = self.fetch_static_map_bytes( + center=center, + markers=[sm_marker], + size=size, + zoom=zoom, + map_type=map_type, + img_format="png", + scale=2, + ) + + return img_bytes \ No newline at end of file diff --git a/week2/community-contributions/book_ticket_agent/tool_box.py b/week2/community-contributions/book_ticket_agent/tool_box.py new file mode 100644 index 0000000..69fa878 --- /dev/null +++ b/week2/community-contributions/book_ticket_agent/tool_box.py @@ -0,0 +1,272 @@ +import base64 +import json +from travel_api import TravelAPI +from api_key_loader import ApiKeyLoader +from map_generator import MapGenerator +from openai import OpenAI +from typing import Any, Dict, List, Optional +from PIL import Image +import io +from io import BytesIO + + +# Internal function specs (simple) used to build OpenAI-compatible tools list +_FUNCTION_SPECS: Dict[str, Dict[str, Any]] = { + "get_flight": { + "name": "get_flight", + "description": ( + "Get flight options from the departure airport (origin), destination airport, date and number of adults." + "Before calling this function, you should have called 'get_airports' to get airport codes for the origin and destination - 2 calls in total. " + "Call this when client ask to book a flight, for example when client asks 'Book ticket to Paris on 2023-01-01'. If the origin or destination city is missing ask client first to provide it." + ), + "parameters": { + "type": "object", + "properties": { + "origin_location_code": { + "type": "string", + "description": "IATA code of the origin airport, e.g. 'MAD'", + }, + "destination_location_code": { + "type": "string", + "description": "IATA code of the destination airport, e.g. 'ATH'", + }, + "departure_date": { + "type": "string", + "description": "Date of departure in 'YYYY-MM-DD'", + }, + "adults": { + "type": "integer", + "description": "Number of adult passengers (default 1)", + } + }, + "required": ["origin_location_code", "destination_location_code", "departure_date"], + "additionalProperties": False + }, + }, + "get_airports": { + "name": "get_airports", + "description": ( + "Get airports for a city name using 'city'. Call this to resolve a city to airports." + "The response contains a list of airport objects. Use the selected airport's 'iataCode' for get_flight." + ), + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "City name to search airports for", + }, + }, + "required": ["city"], + "additionalProperties": False + } + }, + "get_map": { + "name": "get_map", + "description": "Generate a Google Static Map PNG for a list of airport/location for given `city`. Call this function when user ask to show city airports on map", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "City name to search airports for and than show on map", + }, + }, + "required": ["city"], + "additionalProperties": False + } + }, + "get_boarding_pass": { + "name": "get_boarding_pass", + "description": "Generate a boarding pass for a flight. Call this when client asks for boarding pass.", + "parameters": { + "type": "object", + "properties": { + "origin_location_code": { + "type": "string", + "description": "IATA code of the origin airport, e.g. 'MAD'", + }, + "destination_location_code": { + "type": "string", + "description": "IATA code of the destination airport, e.g. 'ATH'", + }, + "departure_date": { + "type": "string", + "description": "Date of departure in 'YYYY-MM-DD'", + }, + "name": { + "type": "string", + "description": "Passenger name", + } + }, + "required": ["origin_location_code", "destination_location_code", "departure_date", "name"], + "additionalProperties": False + }, + } +} + + +def _to_openai_tools(specs: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]: + """Convert simple specs into OpenAI "tools" list schema.""" + tools: List[Dict[str, Any]] = [] + for spec in specs.values(): + tools.append({ + "type": "function", + "function": { + "name": spec["name"], + "description": spec.get("description", ""), + "parameters": spec.get("parameters", {"type": "object"}), + } + }) + return tools + + +def _tool_response(tool_call_id: Optional[str], payload: Dict[str, Any]) -> Dict[str, Any]: + return { + "role": "tool", + "content": json.dumps(payload), + "tool_call_id": tool_call_id, + } + +def _parse_args(raw_args: Any) -> Dict[str, Any]: + if isinstance(raw_args, str): + try: + return json.loads(raw_args) if raw_args else {} + except Exception: + return {} + if isinstance(raw_args, dict): + return raw_args + return {} + +def _extract_tool_call(tool_call: Any): + function = getattr(tool_call, "function", None) or ( + tool_call.get("function") if isinstance(tool_call, dict) else None + ) + name = getattr(function, "name", None) or ( + function.get("name") if isinstance(function, dict) else None + ) + raw_args = getattr(function, "arguments", None) or ( + function.get("arguments") if isinstance(function, dict) else None + ) + call_id = getattr(tool_call, "id", None) or ( + tool_call.get("id") if isinstance(tool_call, dict) else None + ) + return name, _parse_args(raw_args), call_id + + +class ToolBox: + def __init__(self, keys: ApiKeyLoader): + self.travel_api = TravelAPI(keys.get("client_id"), keys.get("client_secret")) + self.map_generator = MapGenerator(keys.get("google_map_api_key")) + self.openai = OpenAI(api_key=keys.get("openai_api_key")) + self.tools = _to_openai_tools(_FUNCTION_SPECS) + self._fn_dispatch = { + "get_flight": self.get_flight, + "get_airports": self.get_airports, + "get_map": self.get_map, + } + + def get_flight(self, origin_location_code, destination_location_code, departure_date, adults=1): + return self.travel_api.get_flight(origin_location_code, destination_location_code, departure_date, + adults=adults) + + def get_airports(self, city): + return self.travel_api.get_airport(city) + + def get_map(self, city): + airports = self.travel_api.get_airport(city) + return airports, self.map_generator.generate(airports) + + def get_toolset(self): + return self.tools + + def get_boarding_pass(self, origin_location_code, destination_location_code, departure_date, name): + image_response = self.openai.images.generate( + model="dall-e-3", + prompt=f"An image representing a boarding pass from {origin_location_code} to {destination_location_code} for {name} and departure date {departure_date}", + size="1024x1024", + n=1, + response_format="b64_json", + ) + image_base64 = image_response.data[0].b64_json + image_data = base64.b64decode(image_base64) + return Image.open(BytesIO(image_data)) + + + def apply(self, message): + """Apply tool calls contained in an assistant message and return a list of tool messages.""" + results: List[Dict[str, Any]] = [] + tool_calls = getattr(message, "tool_calls", None) or [] + if not tool_calls: + return results + + generated_user_message: Optional[str] = None + image = None + + for tool_call in tool_calls: + function_name, arguments, call_id = _extract_tool_call(tool_call) + + if function_name == "get_flight": + origin_location_code = arguments.get("origin_location_code") + destination_location_code = arguments.get("destination_location_code") + departure_date = arguments.get("departure_date") + adults = arguments.get("adults") or 1 + + options = self.get_flight( + origin_location_code, + destination_location_code, + departure_date, + adults=adults, + ) + results.append(_tool_response(call_id, {"flight_options": options})) + + elif function_name == "get_boarding_pass": + origin_location_code = arguments.get("origin_location_code") + destination_location_code = arguments.get("destination_location_code") + departure_date = arguments.get("departure_date") + name = arguments.get("name") + image = self.get_boarding_pass(origin_location_code, destination_location_code, departure_date, name) + results.append(_tool_response(call_id, {"boarding_pass": f"boarding pass for {name} from {origin_location_code} to {destination_location_code} on {departure_date}."})) + if generated_user_message is None: + generated_user_message = ( + f"Here is my boarding pass for {name} from {origin_location_code} to {destination_location_code} on {departure_date}." + ) + + elif function_name == "get_airports": + city = arguments.get("city") + airports = self.get_airports(city) + results.append(_tool_response(call_id, {"airports": airports})) + if generated_user_message is None: + generated_user_message = ( + f"Here are the airports in {city}: {airports} Please help me to select one." + ) + + elif function_name == "get_map": + city = arguments.get("city") + try: + airports, img_bytes = self.get_map(city) + if img_bytes: + try: + pil_img = Image.open(io.BytesIO(img_bytes)) + pil_img.load() + if pil_img.mode not in ("RGB", "RGBA"): + pil_img = pil_img.convert("RGB") + image = pil_img + except Exception: + image = None + results.append(_tool_response(call_id, {"airports": airports})) + if generated_user_message is None: + generated_user_message = ( + f"Here are the airports in {city}: {airports} Please help me to select one." + ) + except Exception as e: + results.append(_tool_response(call_id, {"error": f"get_map failed: {str(e)}"})) + + else: + # Unknown tool: respond so the model can recover gracefully. + results.append(_tool_response(call_id, {"error": f"Unknown tool: {function_name}"})) + + if generated_user_message: + results.append({"role": "user", "content": generated_user_message}) + + return results, image diff --git a/week2/community-contributions/book_ticket_agent/travel_api.py b/week2/community-contributions/book_ticket_agent/travel_api.py new file mode 100644 index 0000000..c9bb648 --- /dev/null +++ b/week2/community-contributions/book_ticket_agent/travel_api.py @@ -0,0 +1,58 @@ +from amadeus import Client, Location, ResponseError + +def filter_other_countries(airports): + country_codes_wights = {} + for airport in airports: + country_code = airport["address"]["countryCode"] + country_codes_wights[country_code] = country_codes_wights.get(country_code, 0) + 1 + country_code = max(country_codes_wights, key=country_codes_wights.get) + return [airport for airport in airports if airport["address"]["countryCode"] == country_code] + + +class TravelAPI: + def __init__(self, client_id, client_secret): + self.client = Client(client_id=client_id, client_secret=client_secret) + + def get_airport(self, search): + try: + airport_locations = self.client.reference_data.locations.get( + keyword=search, + subType=Location.AIRPORT, + ) + return filter_other_countries(airport_locations.data) + except ResponseError as e: + print(f"Amadeus API ResponseError in get_airport: {e}") + return [] + except Exception as e: + print(f"Unexpected error in get_airport: {e}") + return [] + + def get_city(self, search, country_code="IT"): + try: + city_locations = self.client.reference_data.locations.get( + keyword=search, + subType=Location.CITY, + countryCode=country_code + ) + return city_locations.data + except ResponseError as e: + print(f"Amadeus API ResponseError in get_city: {e}") + return [] + except Exception as e: + print(f"Unexpected error in get_city: {e}") + return [] + + def get_flight(self, origin_location_code, destination_location_code, departure_date, adults=1): + try: + offers = self.client.shopping.flight_offers_search.get( + originLocationCode=origin_location_code, + destinationLocationCode=destination_location_code, + departureDate=departure_date, + adults=adults) + return offers.data + except ResponseError as e: + print(f"Amadeus API ResponseError in get_flight: {e}") + return [] + except Exception as e: + print(f"Unexpected error in get_flight: {e}") + return [] \ No newline at end of file diff --git a/week2/community-contributions/day4_linkedin_job_search_assistant.ipynb b/week2/community-contributions/day4_linkedin_job_search_assistant.ipynb new file mode 100644 index 0000000..d1cadc9 --- /dev/null +++ b/week2/community-contributions/day4_linkedin_job_search_assistant.ipynb @@ -0,0 +1,245 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2a8071da", + "metadata": {}, + "source": [ + "Week 2, Day 4 - Use of Tools with openAI - Saurabh Gupta. For any feedback, please mail it to srbmisc@gmail.com, I would love connecting with you to discuss more on this, potentially improving it. Thank you for your read.\n" + ] + }, + { + "cell_type": "markdown", + "id": "08f50ff5", + "metadata": {}, + "source": [ + "# 🔍 LinkedIn Job Search Conversationally with OpenAI and JobSpy\n", + "\n", + "Looking for a job on LinkedIn but tired of manually scrolling through endless listings? This Python project simplifies your job hunt by automating the process of searching LinkedIn job postings based on your **desired role** and **location**.\n", + "\n", + "## 🚀 What It Does\n", + "\n", + "Using the powerful Python scraping library **`jobspy`** [Github Link](https://github.com/speedyapply/JobSpy), this tool fetches **real-time job listings from LinkedIn** tailored to your input. Just provide:\n", + "\n", + "\n", + "- 🧑‍💻 **Job Title** (e.g., *Data Scientist*, *Frontend Developer*)\n", + "- 📍 **Location** (e.g., *New York*, *Remote*, *Berlin*)\n", + "\n", + "…and the script will return up to **3 curated job listings** matching your criteria — complete with **direct links to the job posts** on LinkedIn.\n", + "\n", + "## ✅ Key Features\n", + "\n", + "- 🔎 Real-time LinkedIn job scraping \n", + "- ⚡ Fast and minimalistic – only the top 3 results \n", + "- 🔗 Includes clickable links to the job listings \n", + "- 💬 Simple CLI input — no setup hassles\n", + "\n", + "## 📌 Use Case\n", + "\n", + "Whether you're actively job hunting or just exploring opportunities in your field, this tool helps you **stay updated** with fresh job listings — without opening a browser.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af6d6650", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from jobspy import scrape_jobs\n", + "except:\n", + " %pip install -U python-jobspy\n", + " from jobspy import scrape_jobs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a08eb7af", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import json\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "import gradio as gr\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11a280b1", + "metadata": {}, + "outputs": [], + "source": [ + "load_dotenv(override=True)\n", + "\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "if openai_api_key:\n", + " print(f\"OpenAI API Key exists and begins {openai_api_key[:8]}\")\n", + "else:\n", + " print(\"OpenAI API Key not set\")\n", + " \n", + "MODEL = \"gpt-4.1-mini\"\n", + "openai = OpenAI()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbac4cb6", + "metadata": {}, + "outputs": [], + "source": [ + "def callExternalAPI(title, location) -> str:\n", + " jobs = pd.DataFrame(scrape_jobs(\n", + " site_name=[\"linkedin\"],\n", + " search_term=title,\n", + " location=location,\n", + " results_wanted=3,\n", + " hours_old=48\n", + " ))\n", + " print(f\"Found {len(jobs)} jobs\")\n", + " if len(jobs) > 0:\n", + " df = pd.DataFrame(jobs.loc[:,['title','company','location','date_posted','job_url']])\n", + " df = df.fillna('N/A')\n", + " return df.to_json()\n", + " return 'No results found'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b44a92f6", + "metadata": {}, + "outputs": [], + "source": [ + "system_message = ''' You are a helpful assistant for a Jobs Agency who helps users with job listings available.\n", + "Give short, courteous answers to the users. Tell the user you can bring max 3 job listings at once. Present the job listings to the user in nice Markdown.\n", + "Always be accurate. If you don't know the answer, say so.'''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "339ac21b", + "metadata": {}, + "outputs": [], + "source": [ + "getData_function = {\n", + " \"name\": \"callExternalAPI\",\n", + " \"description\": '''Get the Job listings for the provided job title and/or location. Call this whenever the user needs specific job listings data for the title or location that they provide. Following are few examples - \n", + " 1. Give me the jobs available for Software Engineer at Gurugram, India. - In such case, you can call this function by passing the job_title as Software Engineer and job_location as Gurugram, India.\n", + " 2. Give me the jobs available for Software Engineer. - In such case, you can call this function by passing the job_title as Software Engineer and job_location as blank string.\n", + " 3. Give me the jobs available at Gurugram, India. - In such case, you can call this function by passing the job_title as blank string and job_location as Gurugram, India.\n", + " ''',\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"job_title\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The title or position or designation of the job the user is interested in\",\n", + " },\n", + " \"job_location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The location of the job the user is interested in\",\n", + " }\n", + " },\n", + " \"required\": [\"job_title\",\"job_location\"],\n", + " \"additionalProperties\": False\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "892250a4", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [{\"type\": \"function\", \"function\": getData_function}]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4be6fd80", + "metadata": {}, + "outputs": [], + "source": [ + "# We have to write that function handle_tool_call:\n", + "\n", + "def handle_tool_call(message):\n", + " tool_call = message.tool_calls[0]\n", + " arguments = json.loads(tool_call.function.arguments)\n", + " job_title = arguments.get('job_title')\n", + " job_location = arguments.get('job_location')\n", + " jobs = callExternalAPI(job_title,job_location)\n", + " response = {\n", + " \"role\": \"tool\",\n", + " \"content\": json.dumps({\"job_title\": job_title,\"job_location\": job_location,\"job_listings\": jobs}),\n", + " \"tool_call_id\": tool_call.id\n", + " }\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d0b5b08", + "metadata": {}, + "outputs": [], + "source": [ + "def chat(message, history):\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + history + [{\"role\": \"user\", \"content\": message}]\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools)\n", + "\n", + " if response.choices[0].finish_reason==\"tool_calls\":\n", + " message = response.choices[0].message\n", + " response = handle_tool_call(message)\n", + " messages.append(message)\n", + " messages.append(response)\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages)\n", + " \n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "085b4aae", + "metadata": {}, + "outputs": [], + "source": [ + "gr.ChatInterface(fn=chat, type=\"messages\").launch()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/llms-chat-room/llm_bot.py b/week2/community-contributions/llms-chat-room/llm_bot.py new file mode 100644 index 0000000..a73e7a5 --- /dev/null +++ b/week2/community-contributions/llms-chat-room/llm_bot.py @@ -0,0 +1,115 @@ +import os + +from dotenv import load_dotenv +from openai import OpenAI +import anthropic + +LLM_BOT_CONFIGS = { + "gpt": { + "id": "gpt", + "name": "Giorgio", + "model": "gpt-4.1-nano", + "api_key_env": "OPENAI_API_KEY", + }, + "claude": { + "id": "claude", + "name": "Anna", + "model": "claude-sonnet-4-20250514", + "api_key_env": "ANTHROPIC_API_KEY", + }, + "gemini": { + "id": "gemini", + "name": "Isabella", + "model": "gemini-2.0-flash", + "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/", + "api_key_env": "GOOGLE_API_KEY", + }, + "openai": { + "id": "openai", + "name": "Marco", + "model": "gpt-4o-mini", + "api_key_env": "OPENAI_API_KEY", + }, + "deepseek": { + "id": "deepseek", + "name": "Roberto", + "model": "deepseek-chat", + "base_url": "https://api.deepseek.com", + "api_key_env": "DEEPSEEK_API_KEY" + } +} + + +def load_api_key(): + """Load .env and ensure all required API keys from LLM_BOT_CONFIGS are present. + + - Reads environment variables from .env without overriding already-set envs. + - Collects unique api_key_env names from LLM_BOT_CONFIGS. + - Raises a RuntimeError listing any missing variables. + """ + # Load from .env but do not override variables already set in the environment + load_dotenv(override=False) + + required_env_vars = {cfg["api_key_env"] for cfg in LLM_BOT_CONFIGS.values() if "api_key_env" in cfg} + + missing = [] + for var in sorted(required_env_vars): + val = os.getenv(var) + if not val: + missing.append(var) + + if missing: + raise RuntimeError( + "Missing required API key environment variables: " + + ", ".join(missing) + + ". Please add them to your .env file or export them in your environment." + ) + + +class LLMBot: + + def __init__(self, llm, subject, name=None): + if llm not in LLM_BOT_CONFIGS: + raise ValueError(f"Unknown LLM provider '{llm}'. Available: {', '.join(LLM_BOT_CONFIGS.keys())}") + load_api_key() + self.configuration = LLM_BOT_CONFIGS[llm] + self.subject = subject + api_key = os.getenv(self.configuration["api_key_env"]) + self.llm = llm + self.name = name or self.configuration["name"] + self.system_prompt = \ + (f"You are {self.name}, a person talking to other persons in one room discussing the {subject}.\ + If you are first to speak about the {subject} please describe it in detail to other people and express your opinion.\ + Talk politely and without any prejudices. Be short as other people also would like to express own opinion. If other people already speak about the {subject} \ + please make a comment adding some value to the conversation.") + base_url = self.configuration.get("base_url") + if self.llm == "claude" and not base_url: + self.client = anthropic.Anthropic(api_key=api_key) + else: + self.client = OpenAI(api_key=api_key, base_url=base_url) + + def get_response(self, prompt): + if self.llm == "claude": + # Anthropic API: system prompt is provided separately; messages contain user/assistant turns + messages = [ + {"role": "user", "content": prompt}, + ] + response = self.client.messages.create( + model=self.configuration["model"], + max_tokens=200, + temperature=0.7, + system=self.system_prompt, + messages=messages, + ) + return response.content[0].text + else: + # OpenAI-compatible chat completion API (OpenAI, DeepSeek, Gemini via OpenAI shim) + messages = [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": prompt} + ] + response = self.client.chat.completions.create( + model=self.configuration["model"], + messages=messages, + ) + return response.choices[0].message.content diff --git a/week2/community-contributions/llms-chat-room/llm_chat_room.py b/week2/community-contributions/llms-chat-room/llm_chat_room.py new file mode 100644 index 0000000..746f102 --- /dev/null +++ b/week2/community-contributions/llms-chat-room/llm_chat_room.py @@ -0,0 +1,40 @@ +import random +from llm_bot import LLMBot + +class LLMChatRoom: + def __init__(self, llms, subject): + self.subject = subject + self.bots = [LLMBot(llm, subject) for llm in llms] + self.messages = [] + + def select_speaker(self): + if not self.bots: + return None + return random.choice(self.bots) + + def next_message(self, speaker, prompt): + if speaker: + response = speaker.get_response(prompt) + self.messages.append({"name": speaker.name, "content": response}) + + def get_prompt(self): + prompt = f"Talk about {self.subject}:\n" + for message in self.messages: + prompt += f"{message['name']}: {message['content']}\n" + return prompt + + def get_last_message(self): + return self.messages[-1] if self.messages else None + + def talk(self): + speaker = self.select_speaker() + prompt = self.get_prompt() + self.next_message(speaker, prompt) + + def chat(self, num_turns=10): + self.messages = [] + for _ in range(num_turns): + self.talk() + last_message = self.get_last_message() + print(f"{last_message['name']}: {last_message['content']}") + print("-"* 80) \ No newline at end of file diff --git a/week2/community-contributions/llms-chat-room/llm_multi_chat.ipynb b/week2/community-contributions/llms-chat-room/llm_multi_chat.ipynb new file mode 100644 index 0000000..7c58be2 --- /dev/null +++ b/week2/community-contributions/llms-chat-room/llm_multi_chat.ipynb @@ -0,0 +1,118 @@ +{ + "cells": [ + { + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-10-03T11:54:55.134153Z", + "start_time": "2025-10-03T11:54:55.132338Z" + } + }, + "cell_type": "markdown", + "source": [ + "### LLMs Chat Room\n", + "Several LLMs model discussion the topic inside the chat room\n" + ], + "id": "b317cfdc98c19cf2" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T14:29:44.656272Z", + "start_time": "2025-10-03T14:29:44.261197Z" + } + }, + "cell_type": "code", + "source": "from llm_chat_room import LLMChatRoom", + "id": "33d224807d0b0be6", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "", + "id": "4f00eb9db0c25957" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T14:32:59.878365Z", + "start_time": "2025-10-03T14:32:59.845975Z" + } + }, + "cell_type": "code", + "source": [ + "llms = [\"gpt\",\"claude\",\"gemini\"]\n", + "subject = \"Pets\"\n", + "room = LLMChatRoom(llms, subject)" + ], + "id": "70746449bfd70fcf", + "outputs": [], + "execution_count": 6 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T14:33:15.074067Z", + "start_time": "2025-10-03T14:33:02.158650Z" + } + }, + "cell_type": "code", + "source": "room.chat(5)", + "id": "1b717ae03a6c48c0", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Anna: Hello everyone! I'd love to share my thoughts about pets since we're discussing them today.\n", + "\n", + "I think pets bring such wonderful companionship to our lives. Whether it's a loyal dog who greets you at the door with pure joy, a cat who curls up on your lap during quiet evenings, or even smaller pets like birds or fish that add life to a home - they all offer something special.\n", + "\n", + "What I find most remarkable is how pets teach us responsibility and empathy. Caring for another living being, making sure they're fed, healthy, and loved, really helps us grow as people. And the unconditional love they give back is just incredible.\n", + "\n", + "Of course, I know pet ownership isn't right for everyone - it's a big commitment that requires time, money, and lifestyle adjustments. But for those who can provide a good home, I think the bond between humans and pets is truly one of life's great joys.\n", + "\n", + "What are\n", + "--------------------------------------------------------------------------------\n", + "Isabella: Hello Anna!\n", + "\n", + "I agree wholeheartedly! Pets truly enrich our lives in so many ways. The unconditional love and companionship they offer are invaluable. It's amazing how they teach us responsibility and empathy.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Isabella: I think the best part is that pets come in all shapes and sizes, each with unique quirks and personalities. It is important to consider not only dogs and cats, but also birds, reptiles, and even insects that can bring joy and fulfillment to people's lives. What do you think?\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Giorgio: Hello everyone! If I may add a little, I believe that regardless of the kind of pet, the key is the mutual respect and understanding we develop with them. They truly become part of our families and teach us patience and compassion. It's always wonderful to see how each type of pet has its own way of bonding with people.\n", + "--------------------------------------------------------------------------------\n", + "Giorgio: Hello everyone! I completely agree with Giorgio. Pets, no matter their size or species, have a unique way of connecting with us and enriching our daily lives. Their loyalty and innocence often remind us to appreciate the simple joys and to be more patient and caring. It’s truly a special relationship that benefits both sides.\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], + "execution_count": 7 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/order-processing/order-processing.ipynb b/week2/community-contributions/order-processing/order-processing.ipynb new file mode 100644 index 0000000..3c82fdd --- /dev/null +++ b/week2/community-contributions/order-processing/order-processing.ipynb @@ -0,0 +1,405 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "06cf3063-9f3e-4551-a0d5-f08d9cabb927", + "metadata": {}, + "source": [ + "# Week 2\n", + "\n", + "## Restaurant Order Processing\n", + "\n", + "We going to use 2 LLMs that will talk to each other, one as a customer and another of as a waiter taking order." + ] + }, + { + "cell_type": "code", + "id": "de23bb9e-37c5-4377-9a82-d7b6c648eeb6", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:00.461895Z", + "start_time": "2025-10-03T10:08:00.056696Z" + } + }, + "source": [ + "# imports\n", + "\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "import anthropic\n", + "from IPython.display import Markdown, display, update_display" + ], + "outputs": [], + "execution_count": 1 + }, + { + "cell_type": "code", + "id": "f0a8ab2b-6134-4104-a1bc-c3cd7ea4cd36", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:02.718709Z", + "start_time": "2025-10-03T10:08:02.286926Z" + } + }, + "source": [ + "# import for Google\n", + "# in rare cases, this seems to give an error on some systems, or even crashes the kernel\n", + "# If this happens to you, simply ignore this cell - I give an alternative approach for using Gemini later\n", + "\n", + "import google.generativeai" + ], + "outputs": [], + "execution_count": 2 + }, + { + "cell_type": "code", + "id": "1179b4c5-cd1f-4131-a876-4c9f3f38d2ba", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:05.326683Z", + "start_time": "2025-10-03T10:08:05.317765Z" + } + }, + "source": [ + "# Load environment variables in a file called .env\n", + "# Print the key prefixes to help with any debugging.\n", + "\n", + "load_dotenv(override=True)\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "anthropic_api_key = os.getenv('ANTHROPIC_API_KEY')\n", + "google_api_key = os.getenv('GOOGLE_API_KEY')\n", + "deepseek_api_key = os.getenv('DEEPSEEK_API_KEY')\n", + "\n", + "if openai_api_key:\n", + " print(f\"OpenAI API Key exists and begins {openai_api_key[:8]}\")\n", + "else:\n", + " print(\"OpenAI API Key not set\")\n", + " \n", + "if anthropic_api_key:\n", + " print(f\"Anthropic API Key exists and begins {anthropic_api_key[:7]}\")\n", + "else:\n", + " print(\"Anthropic API Key not set\")\n", + "\n", + "if google_api_key:\n", + " print(f\"Google API Key exists and begins {google_api_key[:8]}\")\n", + "else:\n", + " print(\"Google API Key not set\")\n", + "\n", + "if deepseek_api_key:\n", + " print(f\"Deepseek API Key exists and begins {deepseek_api_key[:8]}\")\n", + "else:\n", + " print(\"Deepseek API Key not set\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OpenAI API Key exists and begins sk-proj-\n", + "Anthropic API Key exists and begins sk-ant-\n", + "Google API Key exists and begins AIzaSyB_\n", + "Deepseek API Key exists and begins sk-97f1d\n" + ] + } + ], + "execution_count": 3 + }, + { + "cell_type": "code", + "id": "797fe7b0-ad43-42d2-acf0-e4f309b112f0", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:09.163792Z", + "start_time": "2025-10-03T10:08:09.106472Z" + } + }, + "source": [ + "# Connect to OpenAI, Anthropic, and DeepSeek\n", + "\n", + "openai = OpenAI()\n", + "\n", + "claude = anthropic.Anthropic()\n", + "\n", + "deepseek = OpenAI(api_key=deepseek_api_key, base_url=\"https://api.deepseek.com\")" + ], + "outputs": [], + "execution_count": 4 + }, + { + "cell_type": "code", + "id": "bcb54183-45d3-4d08-b5b6-55e380dfdf1b", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:11.291179Z", + "start_time": "2025-10-03T10:08:11.288807Z" + } + }, + "source": [ + "# Let's make a conversation between GPT-4.1-mini and Claude-3.5-haiku\n", + "# GPT-4.1-mini - a waiter and Claude-3.5-haiku - a client.\n", + "\n", + "gpt_model = \"gpt-4.1-mini\"\n", + "claude_model = \"claude-3-5-haiku-latest\"\n", + "\n", + "gpt_system = \"You are a waiter in a restaurant. You are very friendly and polite; \\\n", + "you need to take a order from a client using the following menu: \\\n", + " Antipasti (Starters) \\\n", + " - Bruschetta al Pomodoro - €5.00 \\\n", + " - Crostini Toscani - €7.00 \\\n", + " - Tagliere di Salumi - €9.00 \\\n", + " Primi Piatti (First Courses) \\\n", + " - Gnocchetti di Patate con Erbette - €10.00 \\\n", + " - Paccheri con Polipetti - €12.00 \\\n", + " - Risotto ai Frutti di Mare - €15.00 \\\n", + " - Tagliolini al Tartufo - €14.00 \\\n", + " - Zuppa di Cipolle - €8.00 \\\n", + " Secondi Piatti (Second Courses) \\\n", + " - Filetto di Manzo - €18.00 \\\n", + " - Pollo alla Griglia - €12.00 \\\n", + " - Branzino al Forno - €17.00 \\\n", + " Pizze (Main) \\\n", + " - Margherita - €8.00 \\\n", + " - Diavola - €10.00 \\\n", + " - Funghi e Prosciutto - €11.00 \\\n", + " - Vegetariana - €9.50 \\\n", + " Dolci (Dessert) \\\n", + " - Tiramisu - €5.00 \\\n", + " - Panna Cotta - €5.50 \\\n", + " - Gelato - €4.00 \\\n", + " Bevande (Beverages) \\\n", + " - Acqua Naturale / Frizzante - €2.50 \\\n", + " - Birra Artigianale - €4.00 \\\n", + " - Vino della Casa - €5.50 / glass \\\n", + "You need to ask about what client wants to eat and drink, checking menu for availability.\"\n", + "\n", + "claude_system = \"You are a client of the italian restaurant spiking to waiter. You would like to \\\n", + "order something to eat and drink. You like pizza and meat. For drink you like sparking wine. Make a joke \\\n", + "about your favorite food and drink when ordering.\"\n", + "\n", + "gpt_messages = [\"Hello, are you ready to make an order?\"]\n", + "claude_messages = [\"Hi, sure. What pizza do you have?\"]" + ], + "outputs": [], + "execution_count": 5 + }, + { + "cell_type": "code", + "id": "1df47dc7-b445-4852-b21b-59f0e6c2030f", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:15.002601Z", + "start_time": "2025-10-03T10:08:14.999845Z" + } + }, + "source": [ + "def call_gpt():\n", + " messages = [{\"role\": \"system\", \"content\": gpt_system}]\n", + " for gpt, claude in zip(gpt_messages, claude_messages):\n", + " messages.append({\"role\": \"assistant\", \"content\": gpt})\n", + " messages.append({\"role\": \"user\", \"content\": claude})\n", + " completion = openai.chat.completions.create(\n", + " model=gpt_model,\n", + " messages=messages\n", + " )\n", + " return completion.choices[0].message.content" + ], + "outputs": [], + "execution_count": 6 + }, + { + "cell_type": "code", + "id": "9dc6e913-02be-4eb6-9581-ad4b2cffa606", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:26.851569Z", + "start_time": "2025-10-03T10:08:24.701304Z" + } + }, + "source": [ + "call_gpt()" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "'We have four types of pizza available: Margherita for €8.00, Diavola for €10.00, Funghi e Prosciutto for €11.00, and Vegetariana for €9.50. Which one would you like to try?'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 7 + }, + { + "cell_type": "code", + "id": "7d2ed227-48c9-4cad-b146-2c4ecbac9690", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:08:31.932950Z", + "start_time": "2025-10-03T10:08:31.930093Z" + } + }, + "source": [ + "def call_claude():\n", + " # Build the interleaved conversation history for Claude.\n", + " # zip truncates to the shortest list, so it will pair up the common turns only.\n", + " messages = []\n", + " for gpt, claude_message in zip(gpt_messages, claude_messages):\n", + " messages.append({\"role\": \"user\", \"content\": gpt})\n", + " messages.append({\"role\": \"assistant\", \"content\": claude_message})\n", + "\n", + " # Append the latest GPT message as the final user turn ONLY when GPT is one ahead.\n", + " # This is the normal flow (we call call_gpt(), append its output to gpt_messages, then call call_claude()).\n", + " if len(gpt_messages) == len(claude_messages) + 1:\n", + " messages.append({\"role\": \"user\", \"content\": gpt_messages[-1]})\n", + "\n", + " # Validate that the prompt we send to Claude ends with a user turn.\n", + " # If not, we likely called call_claude() out of order (e.g., with equal lengths).\n", + " if not messages or messages[-1][\"role\"] != \"user\":\n", + " raise ValueError(\n", + " \"call_claude expected GPT to be one message ahead so Claude can reply to a user turn. \"\n", + " f\"Got len(gpt_messages)={len(gpt_messages)} and len(claude_messages)={len(claude_messages)}.\"\n", + " )\n", + "\n", + " message = claude.messages.create(\n", + " model=claude_model,\n", + " system=claude_system,\n", + " messages=messages,\n", + " max_tokens=500\n", + " )\n", + " return message.content[0].text" + ], + "outputs": [], + "execution_count": 8 + }, + { + "cell_type": "code", + "id": "01395200-8ae9-41f8-9a04-701624d3fd26", + "metadata": {}, + "source": [ + "call_claude()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "08c2279e-62b0-4671-9590-c82eb8d1e1ae", + "metadata": {}, + "source": [ + "call_gpt()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "0275b97f-7f90-4696-bbf5-b6642bd53cbd", + "metadata": { + "ExecuteTime": { + "end_time": "2025-10-03T10:10:54.588210Z", + "start_time": "2025-10-03T10:10:26.440634Z" + } + }, + "source": [ + "gpt_messages = [\"Hello, are you ready to make an order?\"]\n", + "claude_messages = [\"Hi, yes, sure. What you have special today?\"]\n", + "\n", + "print(f\"Waiter:\\n{gpt_messages[0]}\\n\")\n", + "print(f\"Client:\\n{claude_messages[0]}\\n\")\n", + "\n", + "for i in range(5):\n", + " gpt_next = call_gpt()\n", + " print(f\"Waiter:\\n{gpt_next}\\n\")\n", + " gpt_messages.append(gpt_next)\n", + " \n", + " claude_next = call_claude()\n", + " print(f\"Client:\\n{claude_next}\\n\")\n", + " claude_messages.append(claude_next)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiter:\n", + "Hello, are you ready to make an order?\n", + "\n", + "Client:\n", + "Hi, yes, sure. What you have special today?\n", + "\n", + "Waiter:\n", + "Hello! Today we have some lovely specials that I think you'll enjoy. Our Risotto ai Frutti di Mare is especially fresh and flavorful this evening, and our Tagliolini al Tartufo is a wonderful treat if you love truffle flavors. For starters, the Tagliere di Salumi is a great choice to sample a variety of delicious cured meats. For dessert, our Tiramisu is always a favorite. What type of dishes do you usually enjoy? This way, I can help you pick something perfect! Would you also like to hear about our beverages?\n", + "\n", + "Client:\n", + "*chuckles* Well, I'm definitely a pizza and meat lover! I'd love to hear about your meat options. And since you mentioned beverages - do you have any sparkling wine? Because you know, I always say a good sparkling wine is like a party in a glass, and I'm ready to be the guest of honor! *winks*\n", + "\n", + "I'm thinking maybe a nice pizza with some delicious meat toppings, and of course, a crisp prosecco to go with it. I'm particularly interested in anything with prosciutto or salami. Could you recommend something that would make my taste buds do a little Italian dance?\n", + "\n", + "Waiter:\n", + "*chuckles* I love your enthusiasm! For a pizza with meat toppings, I highly recommend our Funghi e Prosciutto pizza – it comes topped with savory prosciutto and mushrooms, a perfect combination to make your taste buds dance! Another great choice is the Diavola, which has spicy salami if you're in the mood for something with a bit of a kick.\n", + "\n", + "Unfortunately, we don’t have sparkling wine on the menu at the moment, but we do offer a refreshing Birra Artigianale (craft beer) and a lovely Vino della Casa by the glass if you'd like to try either of those. Would you like to go with the Funghi e Prosciutto pizza and perhaps a glass of our house wine, or would you prefer to hear more options? And would you like to start with any starters or go straight to your main?\n", + "\n", + "Client:\n", + "*dramatically places hand on heart* No sparkling wine? Oh, the tragedy! But fear not, I shall survive this Italian culinary adventure. *winks*\n", + "\n", + "I'll definitely go for the Funghi e Prosciutto pizza - sounds like a match made in heaven! And since you don't have my beloved bubbles, I'll take a glass of your house wine. A little vino always makes a pizza taste better, right? \n", + "\n", + "*leans in conspiratorially* You know, I always say pizza and wine are like best friends - they might look different, but they always have each other's back! *chuckles at own joke*\n", + "\n", + "I'll skip the starter and go straight to the main event. My stomach is already doing a happy dance just thinking about that prosciutto and mushroom pizza. Bring it on!\n", + "\n", + "Waiter:\n", + "*smiling* What a fantastic choice! The Funghi e Prosciutto pizza paired with a glass of our lovely house wine will definitely make your taste buds happy – I love your pizza and wine friendship analogy, it’s absolutely spot on!\n", + "\n", + "I'll get your order in right away. If you need anything else while waiting or want to add a dessert later, just let me know. Buon appetito, and enjoy your Italian culinary adventure! 🎉🍕🍷\n", + "\n", + "Client:\n", + "Grazie mille! *raises imaginary wine glass* To the beautiful friendship of pizza and wine, may they always dance together on my plate and in my heart! *winks and chuckles*\n", + "\n", + "I'm looking forward to this delicious adventure. Salute!\n", + "\n", + "Waiter:\n", + "Salute! *raises imaginary glass with you* To the perfect pairing of pizza and wine, may every bite and sip bring you joy and many happy dances! If you need anything else during your meal, I'm right here to help. Enjoy your Funghi e Prosciutto pizza and house wine – a true Italian celebration on your plate! Buon appetito! 😊🍕🍷\n", + "\n", + "Client:\n", + "*takes a playful bow* Grazie! I'll be the most entertained diner in the restaurant tonight! *grins and winks* Can't wait to see this pizza that's about to perform its delicious culinary dance for me! Bring on the prosciutto and mushroom magic! 🇮🇹🍕🥂\n", + "\n", + "Waiter:\n", + "*smiling warmly* I can already tell you're going to make this evening unforgettable with your wonderful spirit! Your pizza with its prosciutto and mushroom magic is on its way, ready to perform that delicious dance just for you. If there's anything else I can do to make your experience even better, don’t hesitate to let me know. Enjoy every bite and sip – it's your night to shine! 🇮🇹🍕🥂\n", + "\n", + "Client:\n", + "*gives a theatrical thumbs up* Fantastico! I'm ready for my pizza performance! Who needs a dance floor when you have a delicious pizza stage? *winks and chuckles*\n", + "\n" + ] + } + ], + "execution_count": 9 + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/pptx_summarizer/Leonardo_da_Vinci.pptx b/week2/community-contributions/pptx_summarizer/Leonardo_da_Vinci.pptx new file mode 100644 index 0000000..0d19192 Binary files /dev/null and b/week2/community-contributions/pptx_summarizer/Leonardo_da_Vinci.pptx differ diff --git a/week2/community-contributions/pptx_summarizer/Malala_Yousafzai.pptx b/week2/community-contributions/pptx_summarizer/Malala_Yousafzai.pptx new file mode 100644 index 0000000..2ae09e8 Binary files /dev/null and b/week2/community-contributions/pptx_summarizer/Malala_Yousafzai.pptx differ diff --git a/week2/community-contributions/pptx_summarizer/Marie_Curie.pptx b/week2/community-contributions/pptx_summarizer/Marie_Curie.pptx new file mode 100644 index 0000000..7f2b260 Binary files /dev/null and b/week2/community-contributions/pptx_summarizer/Marie_Curie.pptx differ diff --git a/week2/community-contributions/pptx_summarizer/Nelson_Mandela.pptx b/week2/community-contributions/pptx_summarizer/Nelson_Mandela.pptx new file mode 100644 index 0000000..f5ad864 Binary files /dev/null and b/week2/community-contributions/pptx_summarizer/Nelson_Mandela.pptx differ diff --git a/week2/community-contributions/pptx_summarizer/Shakespeare_Highlights.pptx b/week2/community-contributions/pptx_summarizer/Shakespeare_Highlights.pptx new file mode 100644 index 0000000..29c7fdb Binary files /dev/null and b/week2/community-contributions/pptx_summarizer/Shakespeare_Highlights.pptx differ diff --git a/week2/community-contributions/pptx_summarizer/pptx summarizer.ipynb b/week2/community-contributions/pptx_summarizer/pptx summarizer.ipynb new file mode 100644 index 0000000..476cb76 --- /dev/null +++ b/week2/community-contributions/pptx_summarizer/pptx summarizer.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ddfa9ae6-69fe-444a-b994-8c4c5970a7ec", + "metadata": {}, + "source": [ + "# Project - PPTX summarizer AI Assistant\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8b50bbe2-c0b1-49c3-9a5c-1ba7efa2bcb4", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import json\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "import gradio as gr\n", + "from IPython.display import Markdown, display, update_display" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "747e8786-9da8-4342-b6c9-f5f69c2e22ae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OpenAI API Key exists and begins sk-proj-\n" + ] + } + ], + "source": [ + "# Initialization\n", + "\n", + "load_dotenv(override=True)\n", + "\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "if openai_api_key:\n", + " print(f\"OpenAI API Key exists and begins {openai_api_key[:8]}\")\n", + "else:\n", + " print(\"OpenAI API Key not set\")\n", + " \n", + "MODEL = \"gpt-4o-mini\"\n", + "openai = OpenAI()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bac7ac31-6ece-4aa1-87eb-329b82f0f45a", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import os\n", + "from pptx import Presentation\n", + "import openai # adapt if you use a different client\n", + "\n", + "class PptxSummary:\n", + " def __init__(self, name: str):\n", + " self.name = name\n", + "\n", + " def summary(self):\n", + " # Load an existing PowerPoint\n", + " prs = Presentation(self.name)\n", + " newtext = \"\"\n", + "\n", + " # print(prs)\n", + " # Loop over all slides\n", + " for i, slide in enumerate(prs.slides, start=1):\n", + " # print(f\"\\n--- Slide {i} ---\")\n", + " newtext += f\"\\n\\n--- Slide {i} ---\"\n", + " \n", + " # Loop over shapes (text boxes, titles, placeholders, etc.)\n", + " for shape in slide.shapes:\n", + " if shape.has_text_frame: # Only shapes that can contain text\n", + " for paragraph in shape.text_frame.paragraphs:\n", + " # Collect text from each run in the paragraph\n", + " text = \"\".join(run.text for run in paragraph.runs)\n", + " # print(text)\n", + " newtext+= \"\\n\"\n", + " newtext += text\n", + " # print(newtext)\n", + " return newtext\n", + "\n", + " \n", + "system_message = \"You are a helpful assistant for a company and you can summarize the given topic based on the given pptx name. \"\n", + "system_message += \"Give short, courteous answers, no more than 10 sentence. \"\n", + "system_message += \"Always be accurate. If the presentation .pptx file does not exist, say so. Respond in markdown.\"\n", + "\n", + "def user_message(path):\n", + " ppt = PptxSummary(path)\n", + " summarization_message = ppt.summary()\n", + " message = \"You need to summarize the a pptx file.\"\n", + " message += f\"The context of that pptx file is here: {summarization_message}\"\n", + " message += \"Give the concise information in small paragraphs. \"\n", + " return message\n", + "\n", + "def pptx_summary(path):\n", + " if os.path.exists(path):\n", + " result = \"The file does not exist\"\n", + " response = openai.chat.completions.create(\n", + " model=MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_message(path)}\n", + " ],\n", + " )\n", + " result = response.choices[0].message.content\n", + " return result\n", + " else:\n", + " return \"The file does not exist\"\n", + "\n", + "\n", + " \n", + "\n", + "# print(\"Current working directory:\", os.getcwd())\n", + "# print(\"Files here:\", os.listdir())\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e4747d6b-d224-4ab3-b9bb-81c423497c81", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Summary of \"Marie Curie: Highlights of a Notable Life\"\n", + "\n", + "The presentation details the life and achievements of Marie Curie, born in Warsaw, Poland in 1867. She pursued her studies in physics and mathematics in Paris, setting the stage for her groundbreaking scientific career.\n", + "\n", + "Marie Curie made history as the first woman to win a Nobel Prize and is renowned for her discovery of radioactivity, alongside her husband, Pierre Curie. Her noteworthy accomplishments include winning Nobel Prizes in both Physics and Chemistry, highlighting her significant contributions to science.\n", + "\n", + "Curie's legacy extends beyond her scientific achievements; she shattered barriers for women in science and her research ultimately aided in the development of medical X-rays. She is celebrated as one of the most influential scientists of all time, leaving an enduring impact on both science and society.\n" + ] + } + ], + "source": [ + "print(pptx_summary(\"Marie_Curie.pptx\"))" + ] + }, + { + "cell_type": "markdown", + "id": "5bd2b368-960c-40b4-adf0-db40fe713cbc", + "metadata": {}, + "source": [ + "# My tools" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8f7c0fda-704b-4e3d-bc69-89986cf5e7a9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The file does not exist\n" + ] + } + ], + "source": [ + "print(pptx_summary(\"bts.pptx\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3f8e54f-af95-4bf6-a202-89e7a3ffdec5", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "36bedabf-a0a7-4985-ad8e-07ed6a55a3a4", + "metadata": {}, + "source": [ + "## Tools\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4afceded-7178-4c05-8fa6-9f2085e6a344", + "metadata": {}, + "outputs": [], + "source": [ + "# There's a particular dictionary structure that's required to describe our function:\n", + "\n", + "summary_function = {\n", + " \"name\": \"pptx_summary\",\n", + " \"description\": \"Get the summary for the given pptx file, you need to call this function, if user asks for a pptx file, if it is outside of the pptx file, tell that you do not know.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"path\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The name of the presentation\",\n", + " },\n", + " },\n", + " \"required\": [\"path\"],\n", + " \"additionalProperties\": False\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "bdca8679-935f-4e7f-97e6-e71a4d4f228c", + "metadata": {}, + "outputs": [], + "source": [ + "# And this is included in a list of tools:\n", + "\n", + "tools = [{\"type\": \"function\", \"function\": summary_function}]" + ] + }, + { + "cell_type": "markdown", + "id": "c3d3554f-b4e3-4ce7-af6f-68faa6dd2340", + "metadata": {}, + "source": [ + "## Getting OpenAI to use our Tool\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ce9b0744-9c78-408d-b9df-9f6fd9ed78cf", + "metadata": {}, + "outputs": [], + "source": [ + "def chat(message, history):\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + history + [{\"role\": \"user\", \"content\": message}]\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools)\n", + "\n", + " if response.choices[0].finish_reason==\"tool_calls\":\n", + " message = response.choices[0].message\n", + " print(f\"message: {message}\")\n", + " response, city = handle_tool_call(message)\n", + " print(f\"city: {city}, response: {response}\")\n", + " messages.append(message)\n", + " messages.append(response)\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages)\n", + " print(f\"response: {response}\")\n", + " \n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b0992986-ea09-4912-a076-8e5603ee631f", + "metadata": {}, + "outputs": [], + "source": [ + "# We have to write that function handle_tool_call:\n", + "\n", + "def handle_tool_call(message):\n", + " tool_call = message.tool_calls[0]\n", + " print(f\"tool_call: {tool_call}\")\n", + " arguments = json.loads(tool_call.function.arguments)\n", + " print(f\"arguments: {arguments}\")\n", + " path = arguments.get('path')\n", + " print(f\"path: {path}\")\n", + " summary = pptx_summary(path)\n", + " # print(f\"price: {price}\")\n", + " response = {\n", + " \"role\": \"tool\",\n", + " \"content\": json.dumps({\"path\": path,\"summary\": summary}),\n", + " \"tool_call_id\": tool_call.id\n", + " }\n", + " print(f\"response: {response}\")\n", + " return response, path" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f4be8a71-b19e-4c2f-80df-f59ff2661f14", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Running on local URL: http://127.0.0.1:7860\n", + "* To create a public link, set `share=True` in `launch()`.\n" + ] + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "message: ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_qgCGcnIRiknlEaT6eGFlPyN4', function=Function(arguments='{\"path\":\"nelson_mandela.pptx\"}', name='pptx_summary'), type='function')])\n", + "tool_call: ChatCompletionMessageFunctionToolCall(id='call_qgCGcnIRiknlEaT6eGFlPyN4', function=Function(arguments='{\"path\":\"nelson_mandela.pptx\"}', name='pptx_summary'), type='function')\n", + "arguments: {'path': 'nelson_mandela.pptx'}\n", + "path: nelson_mandela.pptx\n", + "response: {'role': 'tool', 'content': '{\"path\": \"nelson_mandela.pptx\", \"summary\": \"# Summary of \\\\\"Nelson Mandela: Highlights of a Notable Life\\\\\" \\\\n\\\\nThis presentation covers the life and impact of Nelson Mandela. Born in 1918 in South Africa, Mandela became politically active in the fight against apartheid after studying law. \\\\n\\\\nHe emerged as a prominent leader in the anti-apartheid movement but faced imprisonment for 27 years due to his activism. In 1994, he made history by becoming South Africa\\'s first Black president. \\\\n\\\\nMandela\\'s legacy is profound; he is celebrated as a symbol of peace, reconciliation, and justice. His efforts earned him the Nobel Peace Prize and he continues to inspire human rights movements around the globe.\"}', 'tool_call_id': 'call_qgCGcnIRiknlEaT6eGFlPyN4'}\n", + "city: nelson_mandela.pptx, response: {'role': 'tool', 'content': '{\"path\": \"nelson_mandela.pptx\", \"summary\": \"# Summary of \\\\\"Nelson Mandela: Highlights of a Notable Life\\\\\" \\\\n\\\\nThis presentation covers the life and impact of Nelson Mandela. Born in 1918 in South Africa, Mandela became politically active in the fight against apartheid after studying law. \\\\n\\\\nHe emerged as a prominent leader in the anti-apartheid movement but faced imprisonment for 27 years due to his activism. In 1994, he made history by becoming South Africa\\'s first Black president. \\\\n\\\\nMandela\\'s legacy is profound; he is celebrated as a symbol of peace, reconciliation, and justice. His efforts earned him the Nobel Peace Prize and he continues to inspire human rights movements around the globe.\"}', 'tool_call_id': 'call_qgCGcnIRiknlEaT6eGFlPyN4'}\n", + "response: ChatCompletion(id='chatcmpl-CKna164Iq9BrGm35e19Rqm8K6Yr9N', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='# Summary of \"Nelson Mandela: Highlights of a Notable Life\"\\n\\nThis presentation covers the life and impact of Nelson Mandela. Born in 1918 in South Africa, Mandela became politically active in the fight against apartheid after studying law. \\n\\nHe emerged as a prominent leader in the anti-apartheid movement but faced imprisonment for 27 years due to his activism. In 1994, he made history by becoming South Africa\\'s first Black president. \\n\\nMandela\\'s legacy is profound; he is celebrated as a symbol of peace, reconciliation, and justice. His efforts earned him the Nobel Peace Prize, and he continues to inspire human rights movements around the globe.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))], created=1759073181, model='gpt-4o-mini-2024-07-18', object='chat.completion', service_tier='default', system_fingerprint='fp_560af6e559', usage=CompletionUsage(completion_tokens=134, prompt_tokens=269, total_tokens=403, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)))\n" + ] + } + ], + "source": [ + "gr.ChatInterface(fn=chat, type=\"messages\").launch()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d551ce21-c66c-4f56-88ad-8212f16437ab", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/tsungyulin/reserveTicketDemo.ipynb b/week2/community-contributions/tsungyulin/reserveTicketDemo.ipynb new file mode 100644 index 0000000..0a1d2a0 --- /dev/null +++ b/week2/community-contributions/tsungyulin/reserveTicketDemo.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "27fa33cf", + "metadata": {}, + "outputs": [], + "source": [ + "import openai\n", + "from dotenv import load_dotenv\n", + "import gradio as gr\n", + "import os\n", + "import json\n", + "from datetime import datetime\n", + "\n", + "import httpx\n", + "from fastapi import FastAPI\n", + "import uvicorn\n", + "import threading\n", + "\n", + "load_dotenv('.env',override=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9407192", + "metadata": {}, + "outputs": [], + "source": [ + "app = FastAPI()\n", + "\n", + "@app.post('/mock/ticket')\n", + "def booking(payload:dict):\n", + " dt = datetime.strptime(payload.get('date'), \"%Y/%m/%d\") \n", + " isoStr = dt.date().isoformat()\n", + " return {\"status\": \"success\", \"order_id\": f\"MOCK-FLIGHT-{isoStr}-001\"}\n", + "\n", + "# start server\n", + "def run():\n", + " uvicorn.run(app, host=\"127.0.0.1\", port=8000)\n", + "\n", + "thread = threading.Thread(target=run, daemon=True)\n", + "thread.start()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2229b6db", + "metadata": {}, + "outputs": [], + "source": [ + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "llm = openai.OpenAI(api_key=openai_api_key)\n", + "\n", + "system_message = \"You are a helpful assistant for an Airline called FlightAI. \"\n", + "system_message += \"Give short, courteous answers, no more than 1 sentence. \"\n", + "system_message += \"Always be accurate. If you don't know the answer, say so.\"\n", + "\n", + "async def booking_flight(departure, destination, date):\n", + " print(f\"Book the Flight Automatically, {departure} to {destination} at {date}.\")\n", + " reqBody = {\n", + " \"departure\": departure,\n", + " \"destination\": destination,\n", + " \"date\": date\n", + " }\n", + " async with httpx.AsyncClient() as client:\n", + " res = await client.post('http://127.0.0.1:8000/mock/ticket', json=reqBody)\n", + " print(res.text)\n", + " return res.text\n", + " \n", + "book_function = {\n", + " \"name\": \"booking_flight\",\n", + " \"description\": \"async function for booking the flight ticket for customers and it will return the status and id of flight. Call this function whenever you were asked to book the flight, and you will automatically tell the status of the order and the book number! if customers don't provide their departure or destination or date you should inquire them courteous. Note that the date format you should keep them with %Y/%m/%d. for example when a customer asks 'Please help me book the ticket from to '\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"departure\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city where the customer departure\",\n", + " },\n", + " \"destination\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city that the customer wants to travel to\",\n", + " },\n", + " \"date\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The date of the flight \",\n", + " },\n", + " },\n", + " \"required\": [\"destination\", \"departure\", \"date\"],\n", + " \"additionalProperties\": False\n", + " }\n", + "}\n", + "\n", + "async def handle_tool_call(message):\n", + " tool_call = message.tool_calls[0]\n", + " arguments = json.loads(tool_call.function.arguments)\n", + " departure = arguments.get('departure')\n", + " destination = arguments.get('destination')\n", + " date = arguments.get('date')\n", + " res = await booking_flight(departure, destination, date)\n", + " response = {\n", + " \"role\": \"tool\",\n", + " \"content\": json.dumps(res),\n", + " \"tool_call_id\": tool_call.id\n", + " }\n", + " return response\n", + "\n", + "tools = [{\"type\": \"function\", \"function\": book_function}]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bf9656f", + "metadata": {}, + "outputs": [], + "source": [ + "res = await booking_flight('Taiwan', \"NewYork\", \"2025/12/03\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2924055", + "metadata": {}, + "outputs": [], + "source": [ + "async def chat(message, history):\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + history + [{\"role\": \"user\", \"content\": message}]\n", + " res = llm.chat.completions.create(messages=messages,\n", + " model=\"gpt-4.1-mini\",\n", + " tools=tools)\n", + " \n", + " if res.choices[0].finish_reason == 'tool_calls':\n", + " message = res.choices[0].message\n", + " toolResponse = await handle_tool_call(message)\n", + " messages.append(message)\n", + " messages.append(toolResponse)\n", + " res = llm.chat.completions.create(messages=messages,\n", + " model=\"gpt-4.1-mini\")\n", + "\n", + " return res.choices[0].message.content\n", + "\n", + "gr.ChatInterface(fn=chat,type=\"messages\").launch()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.10.15", + "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.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/tsungyulin/week2 EXERCISE.ipynb b/week2/community-contributions/tsungyulin/week2 EXERCISE.ipynb new file mode 100644 index 0000000..9760684 --- /dev/null +++ b/week2/community-contributions/tsungyulin/week2 EXERCISE.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d006b2ea-9dfe-49c7-88a9-a5a0775185fd", + "metadata": {}, + "source": [ + "# Additional End of week Exercise - week 2\n", + "\n", + "Now use everything you've learned from Week 2 to build a full prototype for the technical question/answerer you built in Week 1 Exercise.\n", + "\n", + "This should include a Gradio UI, streaming, use of the system prompt to add expertise, and the ability to switch between models. Bonus points if you can demonstrate use of a tool!\n", + "\n", + "If you feel bold, see if you can add audio input so you can talk to it, and have it respond with audio. ChatGPT or Claude can help you, or email me if you have questions.\n", + "\n", + "I will publish a full solution here soon - unless someone beats me to it...\n", + "\n", + "There are so many commercial applications for this, from a language tutor, to a company onboarding solution, to a companion AI to a course (like this one!) I can't wait to see your results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05fc552b", + "metadata": {}, + "outputs": [], + "source": [ + "import openai\n", + "import anthropic\n", + "import gradio as gr\n", + "import dotenv\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a07e7793-b8f5-44f4-aded-5562f633271a", + "metadata": {}, + "outputs": [], + "source": [ + "class Chatbot:\n", + " def __init__(self, apiKey, publisher='openai'):\n", + " if publisher not in ['openai', 'claude']:\n", + " raise ValueError(f\"publisher must be openai or claude, but got {publisher}\")\n", + " self.publisher = publisher\n", + " self.systemPrompt = None\n", + " self.historyPrompt = []\n", + " self.llm = openai.OpenAI(api_key=apiKey) if publisher == 'openai' else anthropic.Anthropic(api_key=apiKey)\n", + " \n", + " def setSystemPrompt(self, systemPrompt:str):\n", + " self.systemPrompt = systemPrompt.strip()\n", + " if len(self.historyPrompt) == 0:\n", + " self.historyPrompt.append({\"role\": \"system\", \"content\": f\"{systemPrompt}\"})\n", + " else:\n", + " self.historyPrompt[0] = {\"role\": \"system\", \"content\": f\"{systemPrompt}\"}\n", + " \n", + " def _prompt2obj(self, role:str, prompt:str):\n", + " return {\n", + " \"role\": role,\n", + " \"content\": prompt.strip()\n", + " }\n", + " \n", + " def unpackText(self, chunk):\n", + " text = ''\n", + " if self.publisher == 'openai':\n", + " text = chunk.choices[0].delta.content or ''\n", + " elif self.publisher == 'claude':\n", + " if chunk.type == \"content_block_delta\":\n", + " text = chunk.delta.text or ''\n", + " \n", + " return text\n", + " \n", + " def chat(self, message):\n", + " self.historyPrompt.append(self._prompt2obj(\"user\", message))\n", + " completeReply = \"\"\n", + "\n", + " if self.publisher == 'openai':\n", + " stream = self.llm.chat.completions.create(model='gpt-4o-mini',\n", + " messages=self.historyPrompt,\n", + " stream=True)\n", + " elif self.publisher == 'claude':\n", + " stream = self.llm.messages.create(system=self.historyPrompt[0][\"content\"],\n", + " model=\"claude-sonnet-4-20250514\",\n", + " max_tokens=200,\n", + " messages=self.historyPrompt[1:],\n", + " stream=True)\n", + " \n", + " for chunk in stream:\n", + " completeReply += self.unpackText(chunk)\n", + " yield completeReply\n", + " \n", + " \n", + " self.historyPrompt.append(self._prompt2obj(\"assistant\", completeReply))\n", + " \n", + " def _gradioChatWrapper(self):\n", + " def gradioChatFn(message, history):\n", + " for partial_reply in self.chat(message):\n", + " yield partial_reply\n", + " return gradioChatFn\n", + " \n", + " def getAllPrompt(self):\n", + " return self.historyPrompt\n", + " \n", + " def run(self):\n", + " gradioFn = self._gradioChatWrapper()\n", + " gr.ChatInterface(fn=gradioFn, type=\"messages\").launch()\n", + " \n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fca53e8", + "metadata": {}, + "outputs": [], + "source": [ + "# initial env\n", + "dotenv.load_dotenv(\".env\", override=True)\n", + "openaiKey = os.getenv(\"OPENAI_API_KEY\")\n", + "claudeKey = os.getenv(\"ANTHROPIC_API_KEY\")\n", + "openaiInfo = {\n", + " 'apiKey': openaiKey,\n", + " 'publisher': 'openai'\n", + "}\n", + "claudeInfo = {\n", + " 'apiKey': claudeKey,\n", + " 'publisher': 'claude'\n", + "}\n", + "\n", + "SYSTEM_PROMPT = \"\"\"\n", + "You are a technical experts and responds every question I asked with an explanation.\n", + "\"\"\"\n", + "\n", + "openaiChatbot = Chatbot(**openaiInfo)\n", + "openaiChatbot.setSystemPrompt(SYSTEM_PROMPT)\n", + "openaiChatbot.run()\n", + "\n", + "# claudeChatbot = Chatbot(**claudeInfo)\n", + "# claudeChatbot.setSystemPrompt(SYSTEM_PROMPT)\n", + "# claudeChatbot.run()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59a2ac0f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.10.15", + "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.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/voice_enabled_multi_model_AI_assistanve.ipynb b/week2/community-contributions/voice_enabled_multi_model_AI_assistanve.ipynb new file mode 100644 index 0000000..0345dfc --- /dev/null +++ b/week2/community-contributions/voice_enabled_multi_model_AI_assistanve.ipynb @@ -0,0 +1,646 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7bc4a9cd", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import gradio as gr\n", + "from openai import OpenAI\n", + "import anthropic\n", + "import google.generativeai as genai\n", + "from dotenv import load_dotenv\n", + "\n", + "# Load environment variables\n", + "load_dotenv()\n", + "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n", + "anthropic_api_key = os.getenv('ANTHROPIC_API_KEY')\n", + "google_api_key = os.getenv('GOOGLE_API_KEY')\n", + "\n", + "# Initialize clients\n", + "openai_client = OpenAI(api_key=OPENAI_API_KEY)\n", + "genai.configure(api_key=google_api_key)\n", + "claude_client = anthropic.Anthropic(api_key=anthropic_api_key)\n", + "\n", + "# System prompt - Universal and comprehensive\n", + "SYSTEM_PROMPT = \"\"\"You are a highly capable and versatile AI assistant designed to help with any type of question or task.\n", + "\n", + "Your capabilities span across all domains including but not limited to:\n", + "- Programming, software development, and technology\n", + "- Science, mathematics, and engineering\n", + "- Arts, literature, and creative writing\n", + "- History, philosophy, and social sciences\n", + "- Business, finance, and economics\n", + "- Health, wellness, and lifestyle advice\n", + "- Education and learning support\n", + "- Problem-solving and critical thinking\n", + "- General knowledge and trivia\n", + "- Casual conversation and entertainment\n", + "\n", + "Guidelines:\n", + "- Provide accurate, helpful, and comprehensive responses\n", + "- Adapt your tone and style to match the context of the question\n", + "- Use examples and explanations when helpful\n", + "- Be creative when asked for creative content\n", + "- Be precise and factual when asked for information\n", + "- Ask clarifying questions if the request is ambiguous\n", + "- Admit when you're uncertain and provide the best possible guidance\n", + "- Be conversational, friendly, and supportive\n", + "\n", + "You can help with anything from technical coding problems to creative storytelling, from academic research to casual chat. There are no topic restrictions - feel free to engage with any subject matter the user brings up.\"\"\"\n", + "\n", + "# Model configurations\n", + "model_configs = {\n", + " \"GPT-4o\": {\"provider\": \"openai\", \"model\": \"gpt-4o\"},\n", + " \"GPT-4o-mini\": {\"provider\": \"openai\", \"model\": \"gpt-4o-mini\"},\n", + " \"GPT-3.5-turbo\": {\"provider\": \"openai\", \"model\": \"gpt-3.5-turbo\"},\n", + " \"Claude Sonnet 4\": {\"provider\": \"anthropic\", \"model\": \"claude-sonnet-4-20250514\"},\n", + " \"Gemini 2.0 Flash\": {\"provider\": \"google\", \"model\": \"gemini-2.0-flash-exp\"},\n", + "}\n", + "\n", + "def chat_streaming(message, history, model_name, temperature):\n", + " \"\"\"Main chat function with streaming support\"\"\"\n", + " \n", + " config = model_configs[model_name]\n", + " provider = config[\"provider\"]\n", + " model = config[\"model\"]\n", + " \n", + " # Convert messages format history to API format\n", + " messages = []\n", + " for msg in history:\n", + " if msg[\"role\"] == \"user\":\n", + " messages.append({\"role\": \"user\", \"content\": msg[\"content\"]})\n", + " elif msg[\"role\"] == \"assistant\":\n", + " messages.append({\"role\": \"assistant\", \"content\": msg[\"content\"]})\n", + " messages.append({\"role\": \"user\", \"content\": message})\n", + " \n", + " # Stream based on provider\n", + " if provider == \"openai\":\n", + " stream = openai_client.chat.completions.create(\n", + " model=model,\n", + " messages=[{\"role\": \"system\", \"content\": SYSTEM_PROMPT}] + messages,\n", + " temperature=temperature,\n", + " stream=True\n", + " )\n", + " \n", + " response = \"\"\n", + " for chunk in stream:\n", + " if chunk.choices[0].delta.content:\n", + " response += chunk.choices[0].delta.content\n", + " yield response\n", + " \n", + " elif provider == \"anthropic\":\n", + " response = \"\"\n", + " with claude_client.messages.stream(\n", + " model=model,\n", + " max_tokens=2000,\n", + " temperature=temperature,\n", + " system=SYSTEM_PROMPT,\n", + " messages=messages,\n", + " ) as stream:\n", + " for text in stream.text_stream:\n", + " response += text\n", + " yield response\n", + " \n", + " elif provider == \"google\":\n", + " gemini = genai.GenerativeModel(\n", + " model_name=model,\n", + " system_instruction=SYSTEM_PROMPT,\n", + " )\n", + " \n", + " # Convert history for Gemini\n", + " gemini_history = []\n", + " for msg in history:\n", + " if msg[\"role\"] == \"user\":\n", + " gemini_history.append({\"role\": \"user\", \"parts\": [msg[\"content\"]]})\n", + " elif msg[\"role\"] == \"assistant\":\n", + " gemini_history.append({\"role\": \"model\", \"parts\": [msg[\"content\"]]})\n", + " \n", + " chat = gemini.start_chat(history=gemini_history)\n", + " \n", + " stream = chat.send_message(\n", + " message,\n", + " stream=True,\n", + " generation_config=genai.types.GenerationConfig(temperature=temperature)\n", + " )\n", + " \n", + " response = \"\"\n", + " for chunk in stream:\n", + " response += chunk.text\n", + " yield response\n", + "\n", + "def handle_audio_input(audio):\n", + " \"\"\"Transcribe audio input using Whisper\"\"\"\n", + " if audio is None:\n", + " return \"\"\n", + " \n", + " try:\n", + " audio_file = open(audio, \"rb\")\n", + " transcript = openai_client.audio.transcriptions.create(\n", + " model=\"whisper-1\",\n", + " file=audio_file\n", + " )\n", + " return transcript.text\n", + " except Exception as e:\n", + " return f\"Error transcribing audio: {str(e)}\"\n", + "\n", + "def text_to_speech(text):\n", + " \"\"\"Convert text response to speech\"\"\"\n", + " try:\n", + " response = openai_client.audio.speech.create(\n", + " model=\"tts-1\",\n", + " voice=\"nova\",\n", + " input=text[:4096] # Limit to prevent errors\n", + " )\n", + " \n", + " audio_path = \"response.mp3\"\n", + " response.stream_to_file(audio_path)\n", + " return audio_path\n", + " except Exception as e:\n", + " print(f\"TTS Error: {str(e)}\")\n", + " return None\n", + "\n", + "# Custom CSS for modern, attractive UI\n", + "custom_css = \"\"\"\n", + "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');\n", + "\n", + ".gradio-container {\n", + " font-family: 'Inter', sans-serif !important;\n", + " background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;\n", + "}\n", + "\n", + ":root .dark {\n", + " --background-fill-primary: #f0f0f0;\n", + " --body-background-fill: var(--background-fill-primary);\n", + " --block-background-fill: white !important;\n", + " --block-title-background-fill: #dfe7ff;\n", + " --block-title-text-color:#6366f1;\n", + " --body-text-color: black;\n", + " --button-secondary-text-color:black;\n", + " --input-background-fill:white;\n", + "\n", + " --block-label-background-fill:#dfe7ff;\n", + " --block-label-text-color:#6366f1;\n", + "\n", + " --block-border-color:#eaeaea;\n", + " --input-border-color: #eaeaea;\n", + " --border-color-primary:#eaeaea;\n", + "\n", + " --color-accent-soft: #dfe7ff;\n", + " --border-color-accent-subdued: #98a6cf;\n", + "\n", + " --checkbox-background-color: #eaeaea;\n", + " --checkbox-border-color: #eaeaea;\n", + " --background-fill-secondary:#eaeaea;\n", + "}\n", + "\n", + ".main {\n", + " background: white;\n", + " border-radius: 20px;\n", + " box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n", + " padding: 0 !important;\n", + " overflow: hidden;\n", + "}\n", + "\n", + ".contain {\n", + " padding: 2rem !important;\n", + "}\n", + "\n", + "/* Header Styling */\n", + ".header-section {\n", + " background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n", + " padding: 2.5rem 2rem;\n", + " border-radius: 20px 20px 0 0;\n", + " margin: -2rem 0rem 2rem 0rem;\n", + " color: white;\n", + " box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);\n", + "}\n", + "\n", + ".header-section h1 {\n", + " font-size: 2.5rem !important;\n", + " font-weight: 700 !important;\n", + " margin: 0 0 0.5rem 0 !important;\n", + " color: white !important;\n", + " text-shadow: 2px 2px 4px rgba(0,0,0,0.2);\n", + "}\n", + "\n", + ".header-section p {\n", + " font-size: 1.1rem !important;\n", + " margin: 0.5rem 0 !important;\n", + " color: rgba(255,255,255,0.95) !important;\n", + " font-weight: 400;\n", + "}\n", + "\n", + ".feature-badge {\n", + " display: inline-block;\n", + " background: rgba(255,255,255,0.2);\n", + " padding: 0.4rem 1rem;\n", + " border-radius: 20px;\n", + " margin: 0.3rem;\n", + " font-size: 0.9rem;\n", + " backdrop-filter: blur(10px);\n", + " border: 1px solid rgba(255,255,255,0.3);\n", + "}\n", + "\n", + "/* Sidebar Styling */\n", + ".control-panel {\n", + " background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);\n", + " padding: 1.5rem;\n", + " border-radius: 15px;\n", + " box-shadow: 0 4px 15px rgba(0,0,0,0.1);\n", + " height: 100%;\n", + "}\n", + "\n", + ".control-panel label {\n", + " font-weight: 600 !important;\n", + " font-size: 0.95rem !important;\n", + "}\n", + "\n", + "/* Dropdown Styling */\n", + ".dropdown-container select {\n", + " background: white !important;\n", + " border: 2px solid #e2e8f0 !important;\n", + " border-radius: 10px !important;\n", + " padding: 0.75rem !important;\n", + " font-weight: 500 !important;\n", + " transition: all 0.3s ease !important;\n", + "}\n", + "\n", + ".dropdown-container select:hover {\n", + " border-color: #667eea !important;\n", + " box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;\n", + "}\n", + "\n", + "/* Slider Styling */\n", + "input[type=\"range\"] {\n", + " accent-color: #667eea !important;\n", + "}\n", + "\n", + "/* Button Styling */\n", + ".primary-btn {\n", + " background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;\n", + " border: none !important;\n", + " color: white !important;\n", + " font-weight: 600 !important;\n", + " padding: 0.75rem 2rem !important;\n", + " border-radius: 10px !important;\n", + " box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;\n", + " transition: all 0.3s ease !important;\n", + "}\n", + "\n", + ".primary-btn:hover {\n", + " transform: translateY(-2px) !important;\n", + " box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important;\n", + "}\n", + "\n", + ".secondary-btn {\n", + " background: #e2e8f0 !important;\n", + " border: none !important;\n", + " color: #2d3748 !important;\n", + " font-weight: 600 !important;\n", + " padding: 0.75rem 1.5rem !important;\n", + " border-radius: 10px !important;\n", + " transition: all 0.3s ease !important;\n", + "}\n", + "\n", + ".secondary-btn:hover {\n", + " background: #cbd5e0 !important;\n", + " transform: translateY(-2px) !important;\n", + "}\n", + "\n", + "/* Chatbot Styling */\n", + ".chatbot-container {\n", + " background: white;\n", + " border-radius: 15px;\n", + " border: 2px solid #e2e8f0;\n", + " box-shadow: 0 4px 20px rgba(0,0,0,0.08);\n", + " overflow: hidden;\n", + "}\n", + "\n", + "/* Input Box Styling */\n", + ".message-input textarea {\n", + " border: 2px solid #e2e8f0 !important;\n", + " border-radius: 12px !important;\n", + " padding: 1rem !important;\n", + " font-size: 1rem !important;\n", + " transition: all 0.3s ease !important;\n", + "}\n", + "\n", + ".message-input textarea:focus {\n", + " border-color: #667eea !important;\n", + " box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;\n", + "}\n", + "\n", + "/* Input Row Centering */\n", + ".input-row {\n", + " display: flex !important;\n", + " justify-content: center !important;\n", + " align-items: center !important;\n", + " gap: 1rem !important;\n", + "}\n", + "\n", + ".input-row > * {\n", + " flex-shrink: 0 !important;\n", + "}\n", + "\n", + "/* Audio Components */\n", + ".audio-component {\n", + " background: #f7fafc;\n", + " border: 2px dashed #cbd5e0;\n", + " border-radius: 12px;\n", + " padding: 1rem;\n", + " transition: all 0.3s ease;\n", + "}\n", + "\n", + ".audio-component:hover {\n", + " border-color: #667eea;\n", + " background: #edf2f7;\n", + "}\n", + "\n", + "/* Checkbox Styling */\n", + ".checkbox-label {\n", + " display: flex;\n", + " align-items: center;\n", + " gap: 0.5rem;\n", + " font-weight: 500;\n", + " color: #2d3748;\n", + "}\n", + "\n", + "/* Tips Section */\n", + ".tips-section {\n", + " background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);\n", + " padding: 1.5rem;\n", + " border-radius: 15px;\n", + " margin-top: 2rem;\n", + " border-left: 4px solid #667eea;\n", + "}\n", + "\n", + ".tips-section h3 {\n", + " color: #667eea !important;\n", + " font-weight: 600 !important;\n", + " margin-bottom: 1rem !important;\n", + "}\n", + "\n", + ".tips-section ul {\n", + " list-style: none;\n", + " padding: 0;\n", + "}\n", + "\n", + ".tips-section li {\n", + " padding: 0.5rem 0;\n", + " color: #4a5568 !important;\n", + " font-size: 0.95rem;\n", + "}\n", + "\n", + ".tips-section li:before {\n", + " content: \"→ \";\n", + " color: #667eea;\n", + " font-weight: bold;\n", + " margin-right: 0.5rem;\n", + "}\n", + "\n", + "/* Force black color for strong/bold text */\n", + ".tips-section strong {\n", + " color: #1a202c !important;\n", + "}\n", + "\n", + ".prose * {\n", + " color: inherit !important;\n", + "}\n", + "\n", + ".prose strong {\n", + " color: #1a202c !important;\n", + " font-weight: 600 !important;\n", + "}\n", + "\n", + "/* Responsive Design */\n", + "@media (max-width: 768px) {\n", + " .header-section h1 {\n", + " font-size: 1.8rem !important;\n", + " }\n", + " \n", + " .contain {\n", + " padding: 1rem !important;\n", + " }\n", + "}\n", + "\n", + ".fillable{\n", + " max-width:95% !important;\n", + "}\n", + "#component-5{\n", + " flex-grow:1.1 !important;\n", + "}\n", + ".bubble-wrap.svelte-gjtrl6 {\n", + " background:none !important;\n", + "}\n", + ".bot.svelte-1csv61q.message {\n", + " background-color: white !important;\n", + " border: 1px solid #f3f3f3;\n", + "}\n", + ".options.svelte-y6qw75> li:hover{\n", + " background:white ;\n", + "}\n", + ".options.svelte-y6qw75> .selected{\n", + " background:white ;\n", + "}\n", + "\n", + "\"\"\"\n", + "\n", + "# Build Gradio Interface\n", + "with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:\n", + " \n", + " # Header\n", + " with gr.Row(elem_classes=\"header-section\"):\n", + " with gr.Column():\n", + " gr.HTML(\"\"\"\n", + "

🚀 Voice Enabled Multi Model AI-Assistant

\n", + "

Your intelligent companion for any question - from coding to creativity, science to storytelling!

\n", + "
\n", + " 🤖 7 AI Models\n", + " 🎤 Voice Input\n", + " 🔊 Audio Output\n", + " ⚡ Real-time Streaming\n", + " 🌐 Any Topic\n", + "
\n", + " \"\"\")\n", + " \n", + " with gr.Row():\n", + " # Left Sidebar - Controls\n", + " with gr.Column(scale=1, elem_classes=\"control-panel\"):\n", + " gr.HTML(\"

⚙️ Settings

\")\n", + " \n", + " model_choice = gr.Dropdown(\n", + " choices=list(model_configs.keys()),\n", + " value=\"GPT-4o-mini\",\n", + " label=\"🤖 AI Model\",\n", + " info=\"Select your preferred model\",\n", + " elem_classes=\"dropdown-container\"\n", + " )\n", + " \n", + " temperature = gr.Slider(\n", + " minimum=0,\n", + " maximum=1,\n", + " value=0.7,\n", + " step=0.1,\n", + " label=\"🌡️ Temperature\",\n", + " info=\"Higher = more creative responses\"\n", + " )\n", + " \n", + " gr.HTML(\"

🎙️ Audio Features

\")\n", + " \n", + " audio_input = gr.Audio(\n", + " sources=[\"microphone\"],\n", + " type=\"filepath\",\n", + " label=\"🎤 Voice Input\",\n", + " elem_classes=\"audio-component\"\n", + " )\n", + " \n", + " audio_output_enabled = gr.Checkbox(\n", + " label=\"🔊 Enable Audio Response\",\n", + " value=False,\n", + " elem_classes=\"checkbox-label\"\n", + " )\n", + " \n", + " # Right Side - Chat Interface\n", + " with gr.Column(scale=3):\n", + " chatbot = gr.Chatbot(\n", + " label=\"💬 Conversation\",\n", + " height=550,\n", + " show_copy_button=True,\n", + " type='messages',\n", + " elem_classes=\"chatbot-container\",\n", + " avatar_images=(None, \"https://api.dicebear.com/7.x/bottts/svg?seed=ai\")\n", + " )\n", + " \n", + " with gr.Row(elem_classes=\"input-row\"):\n", + " msg = gr.Textbox(\n", + " label=\"\",\n", + " placeholder=\"💭 Ask me anything - tech help, creative writing, life advice, science, history, or just chat!\",\n", + " scale=5,\n", + " elem_classes=\"message-input\",\n", + " show_label=False\n", + " )\n", + " submit_btn = gr.Button(\"Send 📤\", scale=1, elem_classes=\"primary-btn\")\n", + " \n", + " audio_response = gr.Audio(\n", + " label=\"🔊 Audio Response\", \n", + " visible=False,\n", + " elem_classes=\"audio-component\"\n", + " )\n", + " \n", + " with gr.Row():\n", + " clear_btn = gr.Button(\"🗑️ Clear Chat\", elem_classes=\"secondary-btn\")\n", + " \n", + " # Tips Section\n", + " with gr.Row(elem_classes=\"tips-section\"):\n", + " gr.Markdown(\"\"\"\n", + " ### 💡 What Can I Help You With?\n", + " \n", + " - **Technology & Programming**: Debug code, explain concepts, build projects, learn new languages\n", + " - **Creative Writing**: Stories, poems, scripts, brainstorming ideas, character development\n", + " - **Education & Learning**: Homework help, concept explanations, study guides, tutoring\n", + " - **Business & Career**: Resume writing, business plans, marketing ideas, career advice\n", + " - **Science & Math**: Problem-solving, research assistance, concept explanations\n", + " - **Daily Life**: Recipe suggestions, travel planning, health tips, relationship advice\n", + " - **Entertainment**: Jokes, trivia, games, recommendations for books/movies/music\n", + " - **And Literally Anything Else**: No topic is off-limits - just ask!\n", + " \"\"\")\n", + " \n", + " # Event handlers\n", + " def process_message(message, history, model, temp, audio_enabled):\n", + " \"\"\"Process message and optionally generate audio\"\"\"\n", + " # Add user message to history\n", + " history = history + [{\"role\": \"user\", \"content\": message}]\n", + " \n", + " # Generate text response (streaming)\n", + " bot_message = None\n", + " for response in chat_streaming(message, history[:-1], model, temp):\n", + " bot_message = response\n", + " yield history + [{\"role\": \"assistant\", \"content\": response}], None\n", + " \n", + " # Final history with complete response\n", + " final_history = history + [{\"role\": \"assistant\", \"content\": bot_message}]\n", + " \n", + " # Generate audio if enabled\n", + " if audio_enabled and bot_message:\n", + " audio_path = text_to_speech(bot_message)\n", + " yield final_history, audio_path\n", + " else:\n", + " yield final_history, None\n", + " \n", + " def transcribe_and_send(audio, history, model, temp, audio_enabled):\n", + " \"\"\"Transcribe audio and process message\"\"\"\n", + " text = handle_audio_input(audio)\n", + " if text and text != \"\" and not text.startswith(\"Error\"):\n", + " # Process the message and get results\n", + " for hist, aud in process_message(text, history, model, temp, audio_enabled):\n", + " yield hist, aud\n", + " else:\n", + " # If no text or error, return history unchanged\n", + " yield history, None\n", + "\n", + " # Wire up events\n", + " submit_btn.click(\n", + " fn=process_message,\n", + " inputs=[msg, chatbot, model_choice, temperature, audio_output_enabled],\n", + " outputs=[chatbot, audio_response]\n", + " ).then(lambda: \"\", None, msg)\n", + "\n", + " msg.submit(\n", + " fn=process_message,\n", + " inputs=[msg, chatbot, model_choice, temperature, audio_output_enabled],\n", + " outputs=[chatbot, audio_response]\n", + " ).then(lambda: \"\", None, msg)\n", + "\n", + " # Audio input handler using stop_recording event\n", + " audio_input.stop_recording(\n", + " fn=transcribe_and_send,\n", + " inputs=[audio_input, chatbot, model_choice, temperature, audio_output_enabled],\n", + " outputs=[chatbot, audio_response]\n", + " )\n", + "\n", + " # Clear button clears chat, audio response, and audio input\n", + " clear_btn.click(\n", + " fn=lambda: ([], None, None), \n", + " inputs=None, \n", + " outputs=[chatbot, audio_response, audio_input]\n", + " )\n", + "\n", + " # Toggle audio response visibility\n", + " audio_output_enabled.change(\n", + " fn=lambda x: gr.Audio(visible=x),\n", + " inputs=audio_output_enabled,\n", + " outputs=audio_response\n", + " )\n", + "\n", + "# Launch the app\n", + "if __name__ == \"__main__\":\n", + " demo.launch(share=False, debug=True)" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/week2 EXERCISE.ipynb b/week2/community-contributions/week2 EXERCISE.ipynb new file mode 100644 index 0000000..4240925 --- /dev/null +++ b/week2/community-contributions/week2 EXERCISE.ipynb @@ -0,0 +1,304 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d006b2ea-9dfe-49c7-88a9-a5a0775185fd", + "metadata": {}, + "source": [ + "# Additional End of week Exercise - week 2\n", + "\n", + "Now use everything you've learned from Week 2 to build a full prototype for the technical question/answerer you built in Week 1 Exercise.\n", + "\n", + "This should include a Gradio UI, streaming, use of the system prompt to add expertise, and the ability to switch between models. Bonus points if you can demonstrate use of a tool!\n", + "\n", + "If you feel bold, see if you can add audio input so you can talk to it, and have it respond with audio. ChatGPT or Claude can help you, or email me if you have questions.\n", + "\n", + "I will publish a full solution here soon - unless someone beats me to it...\n", + "\n", + "There are so many commercial applications for this, from a language tutor, to a company onboarding solution, to a companion AI to a course (like this one!) I can't wait to see your results." + ] + }, + { + "cell_type": "markdown", + "id": "b6197b62-57fb-4569-a82d-bd4a15f4471b", + "metadata": {}, + "source": [ + "# Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b538792e-2414-4922-bb51-95c61f6c2128", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import requests\n", + "import json\n", + "from typing import List\n", + "from dotenv import load_dotenv\n", + "from IPython.display import Markdown, display, update_display, Audio\n", + "from openai import OpenAI\n", + "import anthropic\n", + "import base64\n", + "from io import BytesIO\n", + "from PIL import Image\n", + "import gradio as gr" + ] + }, + { + "cell_type": "markdown", + "id": "95afec00-4fe5-4cc9-ae0a-37c6ea4934d6", + "metadata": {}, + "source": [ + "# OpenAI API key" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c23ef55-daad-4ecf-893e-b83e507f2732", + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment variables in a file called .env\n", + "# Print the key prefixes to help with any debugging\n", + "\n", + "load_dotenv(override=True)\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "anthropic_api_key = os.getenv('ANTHROPIC_API_KEY')\n", + "\n", + "\n", + "if openai_api_key:\n", + " print(f\"OpenAI API Key exists and begins {openai_api_key[:8]}\")\n", + "else:\n", + " print(\"OpenAI API Key not set\")\n", + " \n", + "if anthropic_api_key:\n", + " print(f\"Anthropic API Key exists and begins {anthropic_api_key[:7]}\")\n", + "else:\n", + " print(\"Anthropic API Key not set\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e260a7ea-fe79-4222-b6d7-7982f55a52d7", + "metadata": {}, + "outputs": [], + "source": [ + "openai = OpenAI()\n", + "\n", + "claude = anthropic.Anthropic()" + ] + }, + { + "cell_type": "markdown", + "id": "270e937e-aef9-4032-b51e-ffa0fec184a1", + "metadata": {}, + "source": [ + "# SYSTEM prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5639ebd6-d197-43c3-a1c4-4308b117e5ef", + "metadata": {}, + "outputs": [], + "source": [ + "# set up environment\n", + "\n", + "def system_prompt(selected_model):\n", + " return f\"\"\"\n", + " You are a tech expert and know every coding language, and can give \n", + " nice, detailed and simple explanations for the given questions.\n", + " Introduce yourself by saying which model you are every time you answer. For example, this is {selected_model}. \n", + " \n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1d960d3-db2e-4f7c-ab7f-d5816f3bd994", + "metadata": {}, + "outputs": [], + "source": [ + "def talker(message):\n", + " response = openai.audio.speech.create(\n", + " model=\"tts-1\",\n", + " voice=\"onyx\",\n", + " input=message)\n", + "\n", + " audio_stream = BytesIO(response.content)\n", + " output_filename = \"output_audio.mp3\"\n", + " with open(output_filename, \"wb\") as f:\n", + " f.write(audio_stream.read())\n", + "\n", + " display(Audio(output_filename, autoplay=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f39f1bc7-87c2-4b44-8ba7-c320acb33361", + "metadata": {}, + "outputs": [], + "source": [ + "def listener(audio_file):\n", + " with open(audio_file, \"rb\") as audio:\n", + " transcript = openai.audio.transcriptions.create(\n", + " model=\"whisper-1\",\n", + " file=audio\n", + " )\n", + " return transcript.text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f88078a9-49ac-48b1-9f07-5b3977e88a52", + "metadata": {}, + "outputs": [], + "source": [ + "def chat(cleared_entry, history, selected_model):\n", + " messages = [{\"role\": \"system\", \"content\": system_prompt(selected_model)}] + history\n", + "\n", + " print(messages)\n", + " \n", + " if selected_model == \"GPT-4o-mini\":\n", + " stream = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages, stream=True)\n", + " response = \"\"\n", + " \n", + " for chunk in stream:\n", + " try:\n", + " response += chunk.choices[0].delta.content or ''\n", + " \n", + " updated_history = history + [{\"role\": \"assistant\", \"content\": response}]\n", + " # talker(response)\n", + " yield updated_history, None \n", + " except Exception as e:\n", + " print(f\"Streaming error: {e}\")\n", + " yield \"Sorry, there was an error processing your request.\"\n", + " # talker(response)\n", + " elif selected_model == \"Claude-sonnet-4\":\n", + " claude_messages = [{\"role\": msg[\"role\"], \"content\": msg[\"content\"]} for msg in history]\n", + " print(claude_messages)\n", + " result = claude.messages.stream(\n", + " model=\"claude-sonnet-4-20250514\",\n", + " max_tokens=200,\n", + " temperature=0.7,\n", + " system=system_prompt(selected_model), \n", + " messages=claude_messages, \n", + " )\n", + " \n", + " response = \"\"\n", + " with result as stream:\n", + " for text in stream.text_stream:\n", + " response += text\n", + " \n", + " updated_history = history + [{\"role\": \"assistant\", \"content\": response}]\n", + " \n", + " yield updated_history, None \n", + " # talker(response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1863d62-a2e8-454b-9296-710c4e5f9396", + "metadata": {}, + "outputs": [], + "source": [ + "with gr.Blocks() as ui:\n", + "\n", + " gr.Markdown(\"## AI Chat Assistant\")\n", + " gr.Markdown(\"**Select your preferred AI model:**\")\n", + " \n", + " model_dropdown = gr.Dropdown(\n", + " choices=[\"GPT-4o-mini\", \"Claude-sonnet-4\"], \n", + " value=\"GPT-4o-mini\", # default selection\n", + " label=\"Choose Model\"\n", + " )\n", + "\n", + " \n", + " with gr.Row():\n", + " chatbot = gr.Chatbot(height=200, type=\"messages\")\n", + " image_output = gr.Image(height=200)\n", + " with gr.Row():\n", + " entry = gr.Textbox(label=\"Chat with our AI Assistant:\")\n", + " with gr.Row():\n", + " # Audio input for voice messages\n", + " audio_input = gr.Audio(\n", + " sources=[\"microphone\", \"upload\"], \n", + " type=\"filepath\", \n", + " label=\"🎙️ Voice Message\"\n", + " )\n", + " with gr.Row():\n", + " voice_submit = gr.Button(\"Send Voice Message\", variant=\"secondary\")\n", + " clear = gr.Button(\"Clear\")\n", + "\n", + "\n", + " def do_entry(message, history):\n", + " history += [{\"role\":\"user\", \"content\":message}]\n", + " return \"\", history\n", + "\n", + " def process_voice_input(audio_file):\n", + " \"\"\"Convert voice to text and put it in the text box\"\"\"\n", + " if audio_file is not None:\n", + " transcribed_text = listener(audio_file)\n", + " if transcribed_text and not transcribed_text.startswith(\"Error\"):\n", + " return transcribed_text\n", + " return \"\"\n", + "\n", + " entry.submit(do_entry, inputs=[entry, chatbot], outputs=[entry, chatbot]).then(\n", + " chat, inputs=[entry,chatbot, model_dropdown], outputs=[chatbot, image_output]\n", + " )\n", + "\n", + " voice_submit.click(\n", + " process_voice_input,\n", + " inputs=[audio_input],\n", + " outputs=[entry]\n", + " )\n", + " \n", + " clear.click(lambda: None, inputs=None, outputs=chatbot, queue=False)\n", + "\n", + "ui.launch(inbrowser=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46085f59-8945-4a64-9db1-d10cd44284db", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week2/community-contributions/week2_challenge_tripplanner.ipynb b/week2/community-contributions/week2_challenge_tripplanner.ipynb new file mode 100644 index 0000000..7e77d60 --- /dev/null +++ b/week2/community-contributions/week2_challenge_tripplanner.ipynb @@ -0,0 +1,435 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d006b2ea-9dfe-49c7-88a9-a5a0775185fd", + "metadata": {}, + "source": [ + "# Additional End of week Exercise - week 2\n", + "\n", + "Now use everything you've learned from Week 2 to build a full prototype for the technical question/answerer you built in Week 1 Exercise.\n", + "\n", + "This should include a Gradio UI, streaming, use of the system prompt to add expertise, and the ability to switch between models. Bonus points if you can demonstrate use of a tool!\n", + "\n", + "If you feel bold, see if you can add audio input so you can talk to it, and have it respond with audio. ChatGPT or Claude can help you, or email me if you have questions.\n", + "\n", + "I will publish a full solution here soon - unless someone beats me to it...\n", + "\n", + "There are so many commercial applications for this, from a language tutor, to a company onboarding solution, to a companion AI to a course (like this one!) I can't wait to see your results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a07e7793-b8f5-44f4-aded-5562f633271a", + "metadata": {}, + "outputs": [], + "source": [ + "ONLINE TRIP PLANNING ASSISTANT" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ea68f58-8b6f-418d-b04c-98e38147a2c3", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import json\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80fd1847-3f5e-4e9b-8b54-b4ca5923aaa8", + "metadata": {}, + "outputs": [], + "source": [ + "force_dark_mode = \"\"\"\n", + "function refresh() {\n", + " const url = new URL(window.location);\n", + " if (url.searchParams.get('__theme') !== 'dark') {\n", + " url.searchParams.set('__theme', 'dark');\n", + " window.location.href = url.href;\n", + " }\n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "212f6842-6058-4246-8d84-c4bcc1f1a651", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialization\n", + "\n", + "load_dotenv(override=True)\n", + "\n", + "openai_api_key = os.getenv('OPENAI_API_KEY')\n", + "if openai_api_key:\n", + " print(f\"OpenAI API Key exists and begins {openai_api_key[:8]}\")\n", + "else:\n", + " print(\"OpenAI API Key not set\")\n", + " \n", + "MODEL = \"gpt-4o-mini\"\n", + "openai = OpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4980d17-a8a1-4347-9a0c-56a57c93d063", + "metadata": {}, + "outputs": [], + "source": [ + "system_message=\"You are the AI trip planner assistant your job is to analyse the user message and give clear and crisp response based on their queries.\"\n", + "system_message+=\"Only if they are asking you specifically for iternary for their trip destination for the required number of days provided by them in the user message. for example\"\n", + "system_message+=\"user: I want a plan for Ooty trip for 3 days.\" \n", + "system_message+=\"then you need to provide an clear simple iternary for OOty for 3 days. Like Day1 this place, this place Day 2 this place this place in a good format.\"\n", + "system_message+=\"Always be accurate if you don't know the answer say so.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a215d7c8-5c3f-4f10-b492-a70d48814010", + "metadata": {}, + "outputs": [], + "source": [ + "# This function looks rather simpler than the one from my video, because we're taking advantage of the latest Gradio updates\n", + "\n", + "def chat(message, history):\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + history + [{\"role\": \"user\", \"content\": message}]\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages)\n", + " return response.choices[0].message.content\n", + "\n", + "gr.ChatInterface(fn=chat, type=\"messages\",js=force_dark_mode).launch()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe328993-5b14-4909-a2c5-3e0cd1bc8a5e", + "metadata": {}, + "outputs": [], + "source": [ + "#let add mulitmodal feature image \n", + "import base64\n", + "from io import BytesIO\n", + "from PIL import Image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fb145ee-2537-48a4-80b0-7921d79669be", + "metadata": {}, + "outputs": [], + "source": [ + "def artist(city):\n", + " image_response = openai.images.generate(\n", + " model=\"dall-e-3\",\n", + " prompt=f\"An image representing a vacation in {city}, showing tourist spots and everything unique about {city}, in a vibrant pop-art style\",\n", + " size=\"1024x1024\",\n", + " n=1,\n", + " response_format=\"b64_json\",\n", + " )\n", + " image_base64 = image_response.data[0].b64_json\n", + " image_data = base64.b64decode(image_base64)\n", + " return Image.open(BytesIO(image_data))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c475aae4-79a4-4a49-b846-b01b6a67000d", + "metadata": {}, + "outputs": [], + "source": [ + "#now lets implement tools for checking the ticket price and for demo flight booking assitance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c047872-df5c-4ea1-bc43-0c5da596aac7", + "metadata": {}, + "outputs": [], + "source": [ + "# Add this import at the top\n", + "from reportlab.lib.pagesizes import letter\n", + "from reportlab.pdfgen import canvas\n", + "from reportlab.lib.units import inch\n", + "from datetime import datetime\n", + "import io\n", + "import base64\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ceeb60f7-ac79-46d8-bbf3-5fab163d9ccf", + "metadata": {}, + "outputs": [], + "source": [ + "#for checking ticket price\n", + "\n", + "ticket_prices = {\"london\": \"$799\", \"paris\": \"$899\", \"tokyo\": \"$1400\", \"berlin\": \"$499\"}\n", + "\n", + "def get_ticket_price(destination_city):\n", + " print(f\"Tool get_ticket_price called for {destination_city}\")\n", + " city = destination_city.lower()\n", + " return ticket_prices.get(city, \"Unknown\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b92db79-87ba-48ec-840b-a54e3d5cb5cc", + "metadata": {}, + "outputs": [], + "source": [ + "#for flight booking\n", + "# Add the booking function\n", + "def book_flight(destination_city, travel_date, passenger_name=\"Traveler\"):\n", + " print(f\"Tool book_flight called for {destination_city} on {travel_date}\")\n", + " \n", + " # Generate a simple PDF ticket\n", + " buffer = io.BytesIO()\n", + " c = canvas.Canvas(buffer, pagesize=letter)\n", + " width, height = letter\n", + " \n", + " # Draw ticket\n", + " c.setFont(\"Helvetica-Bold\", 24)\n", + " c.drawString(2*inch, height - 2*inch, \"FLIGHT TICKET\")\n", + " \n", + " c.setFont(\"Helvetica\", 12)\n", + " c.drawString(1*inch, height - 3*inch, f\"Passenger: {passenger_name}\")\n", + " c.drawString(1*inch, height - 3.5*inch, f\"Destination: {destination_city.upper()}\")\n", + " c.drawString(1*inch, height - 4*inch, f\"Travel Date: {travel_date}\")\n", + " c.drawString(1*inch, height - 4.5*inch, f\"Booking Reference: BK{datetime.now().strftime('%Y%m%d%H%M%S')}\")\n", + " c.drawString(1*inch, height - 5*inch, f\"Price: {ticket_prices.get(destination_city.lower(), 'N/A')}\")\n", + " c.drawString(1*inch, height - 6*inch, \"Status: CONFIRMED ✓\")\n", + " \n", + " c.save()\n", + " \n", + " pdf_bytes = buffer.getvalue()\n", + " buffer.close()\n", + " \n", + " return base64.b64encode(pdf_bytes).decode(), destination_city, travel_date\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51e9a429-dafa-4fdf-9e30-bef07b78a1b3", + "metadata": {}, + "outputs": [], + "source": [ + "# There's a particular dictionary structure that's required to describe our function:\n", + "\n", + "price_function = {\n", + " \"name\": \"get_ticket_price\",\n", + " \"description\": \"Get the price of a return ticket to the destination city. Call this whenever you need to know the ticket price, for example when a customer asks 'How much is a ticket to this city'\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"destination_city\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city that the customer wants to travel to\",\n", + " },\n", + " },\n", + " \"required\": [\"destination_city\"],\n", + " \"additionalProperties\": False\n", + " }\n", + "}\n", + "\n", + "booking_function = {\n", + " \"name\": \"book_flight\",\n", + " \"description\": \"Book a flight ticket to the destination city for a specific date. Call this when a customer wants to book a flight and provides a travel date.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"destination_city\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The destination city for the flight\",\n", + " },\n", + " \"travel_date\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The date of travel in format YYYY-MM-DD or any date format provided by user\",\n", + " },\n", + " \"passenger_name\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The name of the passenger (optional)\",\n", + " }\n", + " },\n", + " \"required\": [\"destination_city\", \"travel_date\"],\n", + " \"additionalProperties\": False\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0912f99b-1650-4c40-8249-5fe808c85c75", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [\n", + " {\"type\": \"function\", \"function\": price_function},\n", + " {\"type\": \"function\", \"function\": booking_function}\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdc5b499-c6f2-48fd-a6ee-304f7dbe8b0e", + "metadata": {}, + "outputs": [], + "source": [ + "# Update handle_tool_call to handle both tools\n", + "def handle_tool_call(message):\n", + " tool_call = message.tool_calls[0]\n", + " arguments = json.loads(tool_call.function.arguments)\n", + " \n", + " if tool_call.function.name == \"get_ticket_price\":\n", + " city = arguments.get('destination_city')\n", + " price = get_ticket_price(city)\n", + " response = {\n", + " \"role\": \"tool\",\n", + " \"content\": json.dumps({\"destination_city\": city, \"price\": price}),\n", + " \"tool_call_id\": tool_call.id\n", + " }\n", + " return response, city\n", + " \n", + " elif tool_call.function.name == \"book_flight\":\n", + " city = arguments.get('destination_city')\n", + " date = arguments.get('travel_date')\n", + " passenger = arguments.get('passenger_name', 'Traveler')\n", + " pdf_base64, city, date = book_flight(city, date, passenger)\n", + " response = {\n", + " \"role\": \"tool\",\n", + " \"content\": json.dumps({\n", + " \"destination_city\": city,\n", + " \"travel_date\": date,\n", + " \"status\": \"confirmed\",\n", + " \"ticket_pdf\": pdf_base64\n", + " }),\n", + " \"tool_call_id\": tool_call.id\n", + " }\n", + " return response, city" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b319f933-da42-4b78-8a07-fedab965abae", + "metadata": {}, + "outputs": [], + "source": [ + "def chat(message, history):\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + history + [{\"role\": \"user\", \"content\": message}]\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools)\n", + " \n", + " pdf_file = None\n", + " image = None\n", + " \n", + " if response.choices[0].finish_reason == \"tool_calls\":\n", + " message_obj = response.choices[0].message\n", + " tool_response, city = handle_tool_call(message_obj)\n", + " \n", + " # Check which tool was called\n", + " if message_obj.tool_calls[0].function.name == \"get_ticket_price\":\n", + " image = artist(city)\n", + " \n", + " elif message_obj.tool_calls[0].function.name == \"book_flight\":\n", + " tool_content = json.loads(tool_response[\"content\"])\n", + " pdf_base64 = tool_content.get(\"ticket_pdf\")\n", + " \n", + " if pdf_base64:\n", + " # Decode and save PDF\n", + " pdf_bytes = base64.b64decode(pdf_base64)\n", + " pdf_file = f\"ticket_{city}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf\"\n", + " with open(pdf_file, \"wb\") as f:\n", + " f.write(pdf_bytes)\n", + " \n", + " messages.append(message_obj)\n", + " messages.append(tool_response)\n", + " response = openai.chat.completions.create(model=MODEL, messages=messages)\n", + " \n", + " return response.choices[0].message.content, pdf_file, image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49f91127-e282-409d-84b8-8d04eb902c74", + "metadata": {}, + "outputs": [], + "source": [ + "with gr.Blocks() as demo:\n", + " gr.Markdown(\"# 🌍 AI Trip Planner Assistant\")\n", + " \n", + " with gr.Row():\n", + " chatbot = gr.Chatbot(type=\"messages\", height=400)\n", + " image_output = gr.Image(height=400, interactive=False, show_label=False)\n", + " \n", + " with gr.Row():\n", + " msg = gr.Textbox(label=\"Your message\", placeholder=\"Ask about ticket prices or book a flight...\", scale=4)\n", + " send_btn = gr.Button(\"Send\", scale=1)\n", + " \n", + " file_output = gr.File(label=\"📄 Download Your Ticket\", visible=True)\n", + " \n", + " def respond(message, history):\n", + " bot_message, pdf_path, image = chat(message, history)\n", + " history.append({\"role\": \"user\", \"content\": message})\n", + " history.append({\"role\": \"assistant\", \"content\": bot_message})\n", + " return history, \"\", pdf_path, image\n", + " \n", + " msg.submit(respond, [msg, chatbot], [chatbot, msg, file_output, image_output])\n", + " send_btn.click(respond, [msg, chatbot], [chatbot, msg, file_output, image_output])\n", + "\n", + "demo.launch()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23b746f6-94d7-44d7-9dec-b4f4dcdd6b61", + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week3/community-contributions/day5_srb_meeting_minutes_generator.ipynb b/week3/community-contributions/day5_srb_meeting_minutes_generator.ipynb new file mode 100644 index 0000000..d6dd7cc --- /dev/null +++ b/week3/community-contributions/day5_srb_meeting_minutes_generator.ipynb @@ -0,0 +1,167 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7ce4a475", + "metadata": {}, + "source": [ + "Week 3, day 5, meeting minutes generator. Code for Google Collab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2abc87f0", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -q --upgrade torch==2.5.1+cu124 torchvision==0.20.1+cu124 torchaudio==2.5.1+cu124 --index-url https://download.pytorch.org/whl/cu124\n", + "!pip install -q requests bitsandbytes==0.46.0 transformers==4.48.3 accelerate==1.3.0 openai" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83fa62ec", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import requests\n", + "from IPython.display import Markdown, display\n", + "from openai import OpenAI\n", + "from huggingface_hub import login\n", + "from google.colab import userdata\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer, BitsAndBytesConfig\n", + "import torch\n", + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5ee86f8", + "metadata": {}, + "outputs": [], + "source": [ + "# Sign in to HuggingFace Hub\n", + "\n", + "hf_token = userdata.get('HF_TOKEN')\n", + "login(hf_token, add_to_git_credential=True)\n", + "\n", + "# Sign in to OpenAI using Secrets in Colab\n", + "\n", + "openai_api_key = userdata.get('OPENAI_API_KEY')\n", + "openai = OpenAI(api_key=openai_api_key)\n", + "\n", + "\n", + "AUDIO_MODEL = \"whisper-1\"\n", + "LLAMA = \"meta-llama/Meta-Llama-3.1-8B-Instruct\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0dd5a0d", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_meeting_minutes(file_data):\n", + " print(\"calling openai to generate transcript\")\n", + " transcription = openai.audio.transcriptions.create(model=AUDIO_MODEL, file=file_data, response_format=\"text\")\n", + " print(\"transcript generated\")\n", + " print(transcription)\n", + " system_message = \"You are an assistant that produces minutes of meetings from transcripts, with summary, key discussion points, takeaways and action items with owners, in markdown.\"\n", + " user_prompt = f\"Below is an extract transcript of a Denver council meeting. Please write minutes in markdown, including a summary with attendees, location and date; discussion points; takeaways; and action items with owners.\\n{transcription}\"\n", + "\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ]\n", + " quant_config = BitsAndBytesConfig(\n", + " load_in_4bit=True,\n", + " bnb_4bit_use_double_quant=True,\n", + " bnb_4bit_compute_dtype=torch.bfloat16,\n", + " bnb_4bit_quant_type=\"nf4\"\n", + " )\n", + " print(\"calling tokenizer\")\n", + " tokenizer = AutoTokenizer.from_pretrained(LLAMA)\n", + " tokenizer.pad_token = tokenizer.eos_token\n", + " inputs = tokenizer.apply_chat_template(messages, return_tensors=\"pt\").to(\"cuda\")\n", + " streamer = TextStreamer(tokenizer)\n", + " print(\"calling model\")\n", + " model = AutoModelForCausalLM.from_pretrained(LLAMA, device_map=\"auto\", quantization_config=quant_config)\n", + " outputs = model.generate(inputs, max_new_tokens=2000, streamer=streamer)\n", + " return tokenizer.decode(outputs[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a938b681", + "metadata": {}, + "outputs": [], + "source": [ + "def process_binary_file(file_data):\n", + " \"\"\"\n", + " This function receives the uploaded file content as a bytes object.\n", + " You can then process the binary data as needed.\n", + " \"\"\"\n", + " if file_data:\n", + " # Example: Get the size of the binary data\n", + " print(f\"processing file\")\n", + " with open(file_data.name, \"rb\") as f:\n", + " return generate_meeting_minutes(f)\n", + "\n", + " return \"No file uploaded or file is empty.\"\n", + "\n", + "with gr.Blocks() as demo:\n", + " gr.Markdown(\"Upload a file and its binary content will be processed.\")\n", + "\n", + " # Use gr.File with type=\"binary\"\n", + " file_input = gr.File(type=\"filepath\", file_count=\"single\", file_types=[\".mp3\"], label=\"Upload a meeting file\")\n", + "\n", + " # Or use gr.UploadButton with type=\"binary\"\n", + " # upload_button = gr.UploadButton(\"Click to Upload Binary File\", type=\"binary\")\n", + "\n", + " output_text = gr.Textbox(label=\"Processing Result\")\n", + "\n", + " # Link the file_input to the processing function\n", + " file_input.upload(process_binary_file, inputs=file_input, outputs=output_text)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6baed239", + "metadata": {}, + "outputs": [], + "source": [ + "demo.launch(debug=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week4/community-contributions/tsungyulin_code_accelerate/config.py b/week4/community-contributions/tsungyulin_code_accelerate/config.py new file mode 100644 index 0000000..75962e7 --- /dev/null +++ b/week4/community-contributions/tsungyulin_code_accelerate/config.py @@ -0,0 +1,39 @@ +OPENAI_MODEL = "gpt-4o-mini" +CLAUDE_MODEL = "claude-3-5-haiku-20241022" + +OUTPUT_MAX_TOKEN = 2000 + +PYTHON_CODE = ''' +import math + +def pairwise_distance(points_a, points_b): + """ + Compute the pairwise Euclidean distance between two sets of 3D points. + + Args: + points_a: list of (x, y, z) + points_b: list of (x, y, z) + Returns: + A 2D list of shape (len(points_a), len(points_b)) representing distances + """ + distances = [] + for i in range(len(points_a)): + row = [] + for j in range(len(points_b)): + dx = points_a[i][0] - points_b[j][0] + dy = points_a[i][1] - points_b[j][1] + dz = points_a[i][2] - points_b[j][2] + d = math.sqrt(dx * dx + dy * dy + dz * dz) + row.append(d) + distances.append(row) + return distances + + +# Example usage +if __name__ == "__main__": + import random + points_a = [(random.random(), random.random(), random.random()) for _ in range(100)] + points_b = [(random.random(), random.random(), random.random()) for _ in range(100)] + dists = pairwise_distance(points_a, points_b) + print(f"Distance[0][0] = {dists[0][0]:.4f}") +''' diff --git a/week4/community-contributions/tsungyulin_code_accelerate/main.py b/week4/community-contributions/tsungyulin_code_accelerate/main.py new file mode 100644 index 0000000..99156c3 --- /dev/null +++ b/week4/community-contributions/tsungyulin_code_accelerate/main.py @@ -0,0 +1,223 @@ +import dotenv +import os +from openai import OpenAI +from anthropic import Anthropic +import gradio as gr + +# from .config import * + +OPENAI_MODEL = "gpt-4o-mini" +CLAUDE_MODEL = "claude-3-5-haiku-20241022" + +OUTPUT_MAX_TOKEN = 2000 + +CSS = """ + body { + background: #f4f6fa; + font-family: 'Segoe UI', Roboto, sans-serif; + } + + .raw textarea { + border: 1.5px solid #00FFBF !important; + box-shadow: 0 0 10px rgba(229, 115, 115, 0.3); + color: #00FFBF !important; + font-size: 24px; + } + + .optimize textarea { + border: 1.5px solid #FFBF00 !important; + box-shadow: 0 0 10px rgba(129, 199, 132, 0.3); + color: #FFBF00 !important; + font-size: 24px + } + + button { + background: linear-gradient(90deg, #2196f3, #00BFFF); + color: white !important; + font-weight: bold; + border-radius: 8px !important; + transition: all 0.2s ease-in-out; + } + + button:hover { + background: linear-gradient(90deg, #21cbf3, #2196f3); + transform: scale(1.05); + } + + h1 { + text-align: center; + color: #1565c0; + font-size: 38px; + } + """ + +PYTHON_CODE = ''' +import math + +def pairwise_distance(points_a, points_b): + """ + Compute the pairwise Euclidean distance between two sets of 3D points. + + Args: + points_a: list of (x, y, z) + points_b: list of (x, y, z) + Returns: + A 2D list of shape (len(points_a), len(points_b)) representing distances + """ + distances = [] + for i in range(len(points_a)): + row = [] + for j in range(len(points_b)): + dx = points_a[i][0] - points_b[j][0] + dy = points_a[i][1] - points_b[j][1] + dz = points_a[i][2] - points_b[j][2] + d = math.sqrt(dx * dx + dy * dy + dz * dz) + row.append(d) + distances.append(row) + return distances + + +# Example usage +if __name__ == "__main__": + import random + points_a = [(random.random(), random.random(), random.random()) for _ in range(100)] + points_b = [(random.random(), random.random(), random.random()) for _ in range(100)] + dists = pairwise_distance(points_a, points_b) + print(f"Distance[0][0] = {dists[0][0]:.4f}") +''' + + +def main(): + dotenv.load_dotenv(override=True) + os.environ['OPENAI_API_KEY'] = os.getenv( + 'OPENAI_API_KEY', 'your-key-if-not-using-env') + os.environ['ANTHROPIC_API_KEY'] = os.getenv( + 'ANTHROPIC_API_KEY', 'your-key-if-not-using-env') + + # codeReviser = CodeAccelerator('openai', os.getenv('OPENAI_API_KEY')) + codeReviser = CodeAccelerator('anthropic', os.getenv('ANTHROPIC_API_KEY')) + + display_ui(codeReviser) + + +def safe_exec(code_str): + import io + import sys + import time + import ast + # Build the buffer of IO to extract ouput of stdout + stdout_buffer = io.StringIO() + old_stdout = sys.stdout + sys.stdout = stdout_buffer + + try: + tree = ast.parse(code_str) + compiled = compile(tree, filename="", mode="exec") + local_vars = {} + start = time.time() + exec(compiled, {}, local_vars) + exec_time = time.time() - start + print(f"This code spend {exec_time:.8f} seconds\n") + + # recover sys.stdout + sys.stdout = old_stdout + output_text = stdout_buffer.getvalue() + return output_text + + except Exception as e: + sys.stdout = old_stdout + return f"Error: {e}" + + +def display_ui(codeReviser): + def _optimize(pythonCode): + for text in codeReviser.respond(pythonCode): + yield text.replace("```python", "").replace("```", "") + + with gr.Blocks(css=CSS) as ui: + gr.Markdown("# ✨Convert Python code for accelation") + with gr.Row(): + beforeBlock = gr.Textbox( + label="raw python code", value=PYTHON_CODE, lines=20, elem_classes=["raw"]) + afterBlock = gr.Textbox( + label="optimized python code", lines=20, elem_classes=["optimize"]) + with gr.Row(): + convert = gr.Button("Convert code") + with gr.Row(): + rawRunButton = gr.Button("Run raw code") + optRunButton = gr.Button("Run optimized code") + with gr.Row(): + rawOut = gr.TextArea(label="Raw result:", + elem_classes=["raw"]) + optimizeOut = gr.TextArea( + label="Optimize result:", elem_classes=["optimize"]) + + convert.click(_optimize, + inputs=[beforeBlock], outputs=[afterBlock]) + rawRunButton.click(safe_exec, inputs=[beforeBlock], outputs=[rawOut]) + optRunButton.click(safe_exec, inputs=[ + afterBlock], outputs=[optimizeOut]) + + ui.launch(inbrowser=True) + + +class CodeAccelerator: + + def __init__(self, frontierModel: str, apiKey): + self.frontierModel = frontierModel + + if frontierModel == "openai": + self.llm = OpenAI(api_key=apiKey) + elif frontierModel == "anthropic": + self.llm = Anthropic(api_key=apiKey) + else: + raise ValueError(f'frontierModel {frontierModel} is invalid.') + + def _getChatTemplate(self, pythonCode): + _code = pythonCode.strip() + + systemPrompt = ''' + You are an assistant that reimplements Python code in high performance and just spend the fastest possible time for an windows laptop. + Respond only with Python code; use comments sparingly and do not provide any explanation other than occasional comments. + The new Python code response needs to produce an identical output in the fastest possible time. + ''' + userPrompt = f''' + Rewrite this Python code with the fastest possible implementation that produces identical output in the least time. + Respond only with Python code; do not explain your work other than a few comments. + Remember to import all necessary python packages such as numpy.\n\n + + {_code} + ''' + return [ + {"role": "system", "content": systemPrompt}, + {"role": "user", "content": userPrompt} + ] + + def respond(self, pythonCode): + """Generator""" + chatTemplate = self._getChatTemplate(pythonCode) + reply = "" + if self.frontierModel == 'openai': + stream = self.llm.chat.completions.create(messages=chatTemplate, + model=OPENAI_MODEL, + max_tokens=OUTPUT_MAX_TOKEN, + stream=True) + for chunk in stream: + chunkText = chunk.choices[0].delta.content or "" + reply += chunkText + yield reply + elif self.frontierModel == "anthropic": + stream = self.llm.messages.create(model=CLAUDE_MODEL, + system=chatTemplate[0]['content'], + messages=chatTemplate[1:], + max_tokens=OUTPUT_MAX_TOKEN, + stream=True) + + for chunk in stream: + chunkText = chunk.delta.text if chunk.type == "content_block_delta" else "" + reply += chunkText + yield reply + + +if __name__ == "__main__": + main() diff --git a/week4/community-contributions/w4d3_add_models.ipynb b/week4/community-contributions/w4d3_add_models.ipynb new file mode 100644 index 0000000..623e20d --- /dev/null +++ b/week4/community-contributions/w4d3_add_models.ipynb @@ -0,0 +1,361 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a6ab9a2-28a2-445d-8512-a0dc8d1b54e9", + "metadata": {}, + "source": [ + "# Code Generator\n", + "\n", + "The requirement: use a Frontier model to generate high performance C++ code from Python code\n", + "\n", + "- Try adding Gemini to the Closed Source mix\n", + "- Try more open-source models such as CodeLlama and StarCoder, and see if you can get CodeGemma to work\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e610bf56-a46e-4aff-8de1-ab49d62b1ad3", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import io\n", + "import sys\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "# import google.generativeai\n", + "from google import genai\n", + "from google.genai import types\n", + "import anthropic\n", + "import ollama\n", + "from IPython.display import Markdown, display, update_display\n", + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f672e1c-87e9-4865-b760-370fa605e614", + "metadata": {}, + "outputs": [], + "source": [ + "# environment\n", + "\n", + "load_dotenv(override=True)\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')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8aa149ed-9298-4d69-8fe2-8f5de0f667da", + "metadata": {}, + "outputs": [], + "source": [ + "# initialize\n", + "\n", + "openai = OpenAI()\n", + "claude = anthropic.Anthropic()\n", + "# google.generativeai.configure()\n", + "client = genai.Client()\n", + "\n", + "\n", + "OPENAI_MODEL = \"gpt-4o\"\n", + "CLAUDE_MODEL = \"claude-sonnet-4-20250514\"\n", + "GEMINI_MODEL = 'gemini-2.5-flash'\n", + "LLAMA_MODEL = \"llama3.2\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6896636f-923e-4a2c-9d6c-fac07828a201", + "metadata": {}, + "outputs": [], + "source": [ + "system_message = \"You are an assistant that reimplements Python code in high performance C++ for an M1 Mac. \"\n", + "system_message += \"Respond only with C++ code; use comments sparingly and do not provide any explanation other than occasional comments. \"\n", + "system_message += \"The C++ response needs to produce an identical output in the fastest possible time. \"\n", + "system_message += \"Do not include Markdown formatting (```), language tags (cpp), or extra text. \"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7b3546-57aa-4c29-bc5d-f211970d04eb", + "metadata": {}, + "outputs": [], + "source": [ + "def user_prompt_for(python):\n", + " user_prompt = \"Rewrite this Python code in C++ with the fastest possible implementation that produces identical output in the least time. \"\n", + " user_prompt += \"Respond only with C++ code; do not explain your work other than a few comments. \"\n", + " user_prompt += \"\"\"Your response must not contain neither the leading \"```cpp\" nor the trailer: \"```\" strings! \"\"\"\n", + " user_prompt += \"Pay attention to number types to ensure no int overflows. Remember to #include all necessary C++ packages such as iomanip.\\n\\n\"\n", + " user_prompt += python\n", + " return user_prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6190659-f54c-4951-bef4-4960f8e51cc4", + "metadata": {}, + "outputs": [], + "source": [ + "def messages_for(python):\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_prompt_for(python)}\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71e1ba8c-5b05-4726-a9f3-8d8c6257350b", + "metadata": {}, + "outputs": [], + "source": [ + "# write to a file called optimized.cpp\n", + "\n", + "def write_output(cpp):\n", + " with open(\"optimized.cpp\", \"w\") as f:\n", + " f.write(cpp)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1cbb778-fa57-43de-b04b-ed523f396c38", + "metadata": {}, + "outputs": [], + "source": [ + "pi = \"\"\"\n", + "import time\n", + "\n", + "def calculate(iterations, param1, param2):\n", + " result = 1.0\n", + " for i in range(1, iterations+1):\n", + " j = i * param1 - param2\n", + " result -= (1/j)\n", + " j = i * param1 + param2\n", + " result += (1/j)\n", + " return result\n", + "\n", + "start_time = time.time()\n", + "result = calculate(100_000_000, 4, 1) * 4\n", + "end_time = time.time()\n", + "\n", + "print(f\"Result: {result:.12f}\")\n", + "print(f\"Execution Time: {(end_time - start_time):.6f} seconds\")\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3b497b3-f569-420e-b92e-fb0f49957ce0", + "metadata": {}, + "outputs": [], + "source": [ + "python_hard = \"\"\"# Be careful to support large number sizes\n", + "\n", + "def lcg(seed, a=1664525, c=1013904223, m=2**32):\n", + " value = seed\n", + " while True:\n", + " value = (a * value + c) % m\n", + " yield value\n", + "\n", + "def max_subarray_sum(n, seed, min_val, max_val):\n", + " lcg_gen = lcg(seed)\n", + " random_numbers = [next(lcg_gen) % (max_val - min_val + 1) + min_val for _ in range(n)]\n", + " max_sum = float('-inf')\n", + " for i in range(n):\n", + " current_sum = 0\n", + " for j in range(i, n):\n", + " current_sum += random_numbers[j]\n", + " if current_sum > max_sum:\n", + " max_sum = current_sum\n", + " return max_sum\n", + "\n", + "def total_max_subarray_sum(n, initial_seed, min_val, max_val):\n", + " total_sum = 0\n", + " lcg_gen = lcg(initial_seed)\n", + " for _ in range(20):\n", + " seed = next(lcg_gen)\n", + " total_sum += max_subarray_sum(n, seed, min_val, max_val)\n", + " return total_sum\n", + "\n", + "# Parameters\n", + "n = 10000 # Number of random numbers\n", + "initial_seed = 42 # Initial seed for the LCG\n", + "min_val = -10 # Minimum value of random numbers\n", + "max_val = 10 # Maximum value of random numbers\n", + "\n", + "# Timing the function\n", + "import time\n", + "start_time = time.time()\n", + "result = total_max_subarray_sum(n, initial_seed, min_val, max_val)\n", + "end_time = time.time()\n", + "\n", + "print(\"Total Maximum Subarray Sum (20 runs):\", result)\n", + "print(\"Execution Time: {:.6f} seconds\".format(end_time - start_time))\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0be9f47d-5213-4700-b0e2-d444c7c738c0", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_gpt(python):\n", + " stream = openai.chat.completions.create(model=OPENAI_MODEL, messages=messages_for(python), stream=True)\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " fragment = chunk.choices[0].delta.content or \"\"\n", + " reply += fragment\n", + " yield reply" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8669f56b-8314-4582-a167-78842caea131", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_claude(python):\n", + " result = claude.messages.stream(\n", + " model=CLAUDE_MODEL,\n", + " max_tokens=2000,\n", + " system=system_message,\n", + " messages=[{\"role\": \"user\", \"content\": user_prompt_for(python)}],\n", + " )\n", + " reply = \"\"\n", + " with result as stream:\n", + " for text in stream.text_stream:\n", + " reply += text\n", + " yield reply" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97205162", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_gemini(python):\n", + " response = client.models.generate_content_stream(\n", + " model=GEMINI_MODEL,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_message),\n", + " contents=user_prompt_for(python)\n", + " )\n", + "\n", + " reply = \"\"\n", + " for chunk in response:\n", + " fragment = chunk.text or \"\"\n", + " reply += fragment\n", + " yield reply\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f94b13e", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_llama_local(python):\n", + " stream = ollama.chat(\n", + " model='llama3.2',\n", + " messages=messages_for(python),\n", + " stream=True,\n", + " )\n", + "\n", + " reply = \"\"\n", + " # Iterate through the streamed chunks and print the content\n", + " for chunk in stream:\n", + " #print(chunk['message']['content'], end='', flush=True)\n", + " if 'content' in chunk['message']:\n", + " fragment = chunk['message']['content']\n", + " reply += fragment\n", + " yield reply\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f1ae8f5-16c8-40a0-aa18-63b617df078d", + "metadata": {}, + "outputs": [], + "source": [ + "def optimize(python, model):\n", + " if model==\"GPT\":\n", + " result = stream_gpt(python)\n", + " elif model==\"Claude\":\n", + " result = stream_claude(python)\n", + " elif model==\"Gemini\":\n", + " result = stream_gemini(python)\n", + " elif model==\"Llama\":\n", + " result = stream_llama_local(python)\n", + "\n", + " else:\n", + " raise ValueError(\"Unknown model\")\n", + " for stream_so_far in result:\n", + " yield stream_so_far" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1ddb38e-6b0a-4c37-baa4-ace0b7de887a", + "metadata": {}, + "outputs": [], + "source": [ + "with gr.Blocks() as ui:\n", + " with gr.Row():\n", + " python = gr.Textbox(label=\"Python code:\", lines=10, value=python_hard)\n", + " cpp = gr.Textbox(label=\"C++ code, copy it here: https://www.programiz.com/cpp-programming/online-compiler/\", lines=10)\n", + " with gr.Row():\n", + " model = gr.Dropdown([\"GPT\", \"Claude\", \"Gemini\", \"Llama\"], label=\"Select model\", value=\"GPT\")\n", + " convert = gr.Button(\"Convert code\")\n", + "\n", + " convert.click(optimize, inputs=[python, model], outputs=[cpp])\n", + "\n", + "ui.launch(inbrowser=True)" + ] + } + ], + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week4/community-contributions/w4d3_trade_generator_docstring.ipynb b/week4/community-contributions/w4d3_trade_generator_docstring.ipynb new file mode 100644 index 0000000..2889050 --- /dev/null +++ b/week4/community-contributions/w4d3_trade_generator_docstring.ipynb @@ -0,0 +1,499 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a6ab9a2-28a2-445d-8512-a0dc8d1b54e9", + "metadata": {}, + "source": [ + "# Trading Code Generator\n", + "\n", + "A code generator that writes trading code to buy and sell equities in a simulated environment, based on a given API\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e610bf56-a46e-4aff-8de1-ab49d62b1ad3", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "import sys\n", + "import io\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "from google import genai\n", + "from google.genai import types\n", + "import anthropic\n", + "import ollama\n", + "import gradio as gr\n", + "import requests\n", + "from typing import Any" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f672e1c-87e9-4865-b760-370fa605e614", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "# environment\n", + "\n", + "load_dotenv(override=True)\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')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8aa149ed-9298-4d69-8fe2-8f5de0f667da", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "# initialize\n", + "\n", + "openai = OpenAI()\n", + "claude = anthropic.Anthropic()\n", + "client = genai.Client()\n", + "\n", + "\n", + "OPENAI_MODEL = \"gpt-4o\"\n", + "CLAUDE_MODEL = \"claude-sonnet-4-20250514\"\n", + "GEMINI_MODEL = 'gemini-2.5-flash'\n", + "LLAMA_MODEL = \"llama3.2\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36b0a6f6", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "system_message = \"\"\"\n", + "You are an effective programming assistant specialized to generate Python code based on the inputs.\n", + "Respond only with Python code; use comments sparingly and do not provide any explanation other than occasional comments.\n", + "Do not include Markdown formatting (```), language tags (python), or extra text.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7b3546-57aa-4c29-bc5d-f211970d04eb", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def get_user_prompt_for_trade_code_generation(currency, wallet_balance):\n", + " wallet_balance = str(wallet_balance)\n", + "\n", + " user_prompt = \"\"\"\n", + " Create a simple Crypto trading engine Python code.\n", + " The engine will sell or buy the given crypto currency against USDT (Tether) based on the available wallet balance\n", + " This should be a simple Python code, not a function\n", + " The currency is: {}\n", + " The wallet balance is: {}\"\"\".format(currency, wallet_balance)\n", + " user_prompt += \"\"\"\n", + " Output will be a text containing the followings:\n", + " - advice to sell or buy\n", + " - amount in USDT\n", + " Rules you have to apply in the code:\n", + " - compose symbol: convert the input `crypto_currency` argument to upper case and concatenate it to string \"USDT\"\n", + " - compose url passing the previously composed symbol: `url = f\"https://data-api.binance.vision/api/v3/ticker/24hr?symbol={symbol}`\n", + " - call the api from with this url, expect to get the following json response, for example:\n", + " {'symbol': 'BTCUSDT',\n", + " 'priceChange': '1119.99000000',\n", + " 'priceChangePercent': '0.969',\n", + " 'weightedAvgPrice': '116314.23644195',\n", + " 'prevClosePrice': '115600.00000000',\n", + " 'lastPrice': '116719.99000000',\n", + " 'lastQty': '0.05368000',\n", + " 'bidPrice': '116719.99000000',\n", + " 'bidQty': '2.81169000',\n", + " 'askPrice': '116720.00000000',\n", + " 'askQty': '3.46980000',\n", + " 'openPrice': '115600.00000000',\n", + " 'highPrice': '117286.73000000',\n", + " 'lowPrice': '114737.11000000',\n", + " 'volume': '12500.51369000',\n", + " 'quoteVolume': '1453987704.98443060',\n", + " 'openTime': 1758015394001,\n", + " 'closeTime': 1758101794001,\n", + " 'firstId': 5236464586,\n", + " 'lastId': 5238628513,\n", + " 'count': 2163928}\n", + " - build a logic based on the retrieving values which can decide whether the engine should sell or buy he given crypto currency\n", + " - in the logic the code should also decide what is the confident level of selling or buying.\n", + " - if the confident level is high the `amount` should be higher (closer to the `current_wallet_balance`)\n", + " - if the confident level is lower then the amount should be lower as well\n", + " - error handling:\n", + " - if the api call returns with a json hving `code`, `msg` keys in it (eg. 'code': -1121, 'msg': 'Invalid symbol.') then handle this error message\n", + " Response rule: in your response do not include Markdown formatting (```), language tags (python), or extra text.\n", + " \"\"\"\n", + " return user_prompt\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5030fdf5", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def get_user_prompt_for_docstring_generation(python_code):\n", + " return f\"\"\"\n", + " Consider the following Python code: \\n\\n\n", + " {python_code} \\n\\n\n", + "\n", + " Generate a docstring comment around this code and it alongside with the Python code. \\n\n", + " Response rule: in your response do not include Markdown formatting (```), language tags (python), or extra text.\n", + "\n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dc065c2", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def trade_gpt(currency, wallet_balance):\n", + " completion = openai.chat.completions.create(\n", + " model=OPENAI_MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": get_user_prompt_for_trade_code_generation(\n", + " currency,\n", + " wallet_balance\n", + " )\n", + " }\n", + " ]\n", + " )\n", + " return completion.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b402c67", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def code_comment_gpt(python_code):\n", + " completion = openai.chat.completions.create(\n", + " model=OPENAI_MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": get_user_prompt_for_docstring_generation(python_code)}\n", + " ]\n", + " )\n", + " return completion.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dc80287", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def trade_cloude(currency, wallet_balance):\n", + " message = claude.messages.create(\n", + " model=CLAUDE_MODEL,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " system=system_message,\n", + " messages=[\n", + " {\"role\": \"user\", \"content\": get_user_prompt_for_trade_code_generation(\n", + " currency,\n", + " wallet_balance\n", + " )\n", + " },\n", + " ],\n", + " )\n", + "\n", + " return message.content[0].text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90eb9547", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def code_comment_cloude(python_code):\n", + " message = claude.messages.create(\n", + " model=CLAUDE_MODEL,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " system=system_message,\n", + " messages=[\n", + " {\"role\": \"user\", \"content\": get_user_prompt_for_docstring_generation(python_code)\n", + " },\n", + " ],\n", + " )\n", + "\n", + " return message.content[0].text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b94fbd55", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\n", + "def trade_gemini(currency, wallet_balance):\n", + " response = client.models.generate_content(\n", + " model=GEMINI_MODEL,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_message),\n", + " contents=get_user_prompt_for_trade_code_generation(\n", + " currency,\n", + " wallet_balance\n", + " )\n", + " )\n", + "\n", + " return response.text\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f83ef7b8", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\n", + "def code_comment_gemini(python_code):\n", + " response = client.models.generate_content(\n", + " model=GEMINI_MODEL,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_message),\n", + " contents=get_user_prompt_for_docstring_generation(python_code)\n", + " )\n", + "\n", + " return response.text\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6737962d", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def trade_llama(currency, wallet_balance):\n", + " completion = ollama.chat(\n", + " model=LLAMA_MODEL,\n", + " messages=[\n", + " {\"role\": \"user\", \"content\": get_user_prompt_for_trade_code_generation(\n", + " currency,\n", + " wallet_balance\n", + " )\n", + " },\n", + " ],\n", + " )\n", + "\n", + " return completion['message']['content']\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b815aa07", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def code_comment_llama(python_code):\n", + " completion = ollama.chat(\n", + " model=LLAMA_MODEL,\n", + " messages=[\n", + " {\"role\": \"user\", \"content\": get_user_prompt_for_docstring_generation(python_code)},\n", + " ],\n", + " )\n", + "\n", + " return completion['message']['content']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9e07437", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def generate_python_code(input_model, currency, wallet_balance):\n", + " model_mapping = {\"GPT\": trade_gpt(currency, wallet_balance),\n", + " \"Claude\": trade_cloude(currency, wallet_balance),\n", + " \"Gemini\": trade_gemini(currency, wallet_balance),\n", + " \"Llama\": trade_llama(currency, wallet_balance)}\n", + " try:\n", + " return model_mapping[input_model]\n", + " except KeyError as e:\n", + " print(f\"{e}: {input_model} is not valid\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "016fed0e", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def comment_python_code(input_model, python_code):\n", + " model_mapping = {\"GPT\": code_comment_gpt(python_code),\n", + " \"Claude\": code_comment_cloude(python_code),\n", + " \"Gemini\": code_comment_gemini(python_code),\n", + " \"Llama\": code_comment_llama(python_code)\n", + " }\n", + " try:\n", + " return model_mapping[input_model]\n", + " except KeyError as e:\n", + " print(f\"{e}: {input_model} is not valid\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e224a715", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def execute_python(code):\n", + " code = code.replace('```python\\n','').replace('```','')\n", + " try:\n", + " output = io.StringIO()\n", + " sys.stdout = output\n", + " exec(code)\n", + " finally:\n", + " sys.stdout = sys.__stdout__\n", + " return output.getvalue()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea96a88d", + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "with gr.Blocks() as ui:\n", + " gr.Markdown(\"\"\"It generate Trading Python code, which will recommend you whether sell or buy a given crypto currency at its current price.\n", + " Based on the confindency level of the prediction it will recommend what amount should be placed from your available wallet balance\"\"\")\n", + " with gr.Row():\n", + " crypto_currency = gr.Dropdown([\"BTC\", \"ETH\", \"SOL\"], label=\"The Crypto cyrrency\")\n", + " wallet_balance = gr.Number(label=\"Enter a number\")\n", + " model = gr.Dropdown([\"GPT\", \"Claude\", \"Gemini\", \"Llama\"], label=\"Select model\", value=\"GPT\")\n", + " with gr.Row():\n", + " generate_python_code_bt = gr.Button(\"Genarate Python code\")\n", + " with gr.Row():\n", + " with gr.Column():\n", + " python = gr.TextArea(label=\"Python Code\")\n", + " python_comment = gr.Button(\"Comment Python code\")\n", + " python_run = gr.Button(\"Run Python code\")\n", + " with gr.Row():\n", + " result_out = gr.TextArea(label=\"Trading advice\")\n", + "\n", + " generate_python_code_bt.click(generate_python_code, inputs=[model, crypto_currency, wallet_balance], outputs=[python])\n", + " python_comment.click(comment_python_code, inputs=[model, python], outputs=python)\n", + " python_run.click(execute_python, inputs=[python], outputs=result_out)\n", + "\n", + "ui.launch(inbrowser=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "", + "version": "" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week4/community-contributions/w4d3_unit_test.ipynb b/week4/community-contributions/w4d3_unit_test.ipynb new file mode 100644 index 0000000..eb9a10f --- /dev/null +++ b/week4/community-contributions/w4d3_unit_test.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a6ab9a2-28a2-445d-8512-a0dc8d1b54e9", + "metadata": {}, + "source": [ + "# Unit test Generator\n", + "\n", + "Create unit tests on the Python code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e610bf56-a46e-4aff-8de1-ab49d62b1ad3", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "from google import genai\n", + "from google.genai import types\n", + "import anthropic\n", + "import ollama\n", + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f672e1c-87e9-4865-b760-370fa605e614", + "metadata": {}, + "outputs": [], + "source": [ + "# environment\n", + "\n", + "load_dotenv(override=True)\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')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8aa149ed-9298-4d69-8fe2-8f5de0f667da", + "metadata": {}, + "outputs": [], + "source": [ + "# initialize\n", + "\n", + "openai = OpenAI()\n", + "claude = anthropic.Anthropic()\n", + "client = genai.Client()\n", + "\n", + "\n", + "OPENAI_MODEL = \"gpt-4o\"\n", + "CLAUDE_MODEL = \"claude-sonnet-4-20250514\"\n", + "GEMINI_MODEL = 'gemini-2.5-flash'\n", + "LLAMA_MODEL = \"llama3.2\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6896636f-923e-4a2c-9d6c-fac07828a201", + "metadata": {}, + "outputs": [], + "source": [ + "system_message = \"\"\"\n", + "You are an effective programming assistant specialized to generate Python code based on the inputs.\n", + "Respond only with Python code; use comments sparingly and do not provide any explanation other than occasional comments.\n", + "Do not include Markdown formatting (```), language tags (python), or extra text \\n.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7b3546-57aa-4c29-bc5d-f211970d04eb", + "metadata": {}, + "outputs": [], + "source": [ + "def user_prompt_for_unit_test(python):\n", + " user_prompt = f\"\"\"\n", + " Consider the following Python code: \\n\\n\n", + " {python} \\n\\n\n", + "\n", + " Generate a unit test around this code and it alongside with the Python code. \\n\n", + " Response rule: in your response do not include Markdown formatting (```), language tags (python), or extra text.\n", + "\n", + " \"\"\"\n", + " return user_prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6190659-f54c-4951-bef4-4960f8e51cc4", + "metadata": {}, + "outputs": [], + "source": [ + "def messages_for_unit_test(python):\n", + " return [\n", + " {\"role\": \"system\", \"content\": system_message},\n", + " {\"role\": \"user\", \"content\": user_prompt_for_unit_test(python)}\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3b497b3-f569-420e-b92e-fb0f49957ce0", + "metadata": {}, + "outputs": [], + "source": [ + "python_hard = \"\"\"\n", + "\n", + "def lcg(seed, a=1664525, c=1013904223, m=2**32):\n", + " value = seed\n", + " while True:\n", + " value = (a * value + c) % m\n", + " yield value\n", + "\n", + "def max_subarray_sum(n, seed, min_val, max_val):\n", + " lcg_gen = lcg(seed)\n", + " random_numbers = [next(lcg_gen) % (max_val - min_val + 1) + min_val for _ in range(n)]\n", + " max_sum = float('-inf')\n", + " for i in range(n):\n", + " current_sum = 0\n", + " for j in range(i, n):\n", + " current_sum += random_numbers[j]\n", + " if current_sum > max_sum:\n", + " max_sum = current_sum\n", + " return max_sum\n", + "\n", + "def total_max_subarray_sum(n, initial_seed, min_val, max_val):\n", + " total_sum = 0\n", + " lcg_gen = lcg(initial_seed)\n", + " for _ in range(20):\n", + " seed = next(lcg_gen)\n", + " total_sum += max_subarray_sum(n, seed, min_val, max_val)\n", + " return total_sum\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0be9f47d-5213-4700-b0e2-d444c7c738c0", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_gpt(python):\n", + " stream = openai.chat.completions.create(model=OPENAI_MODEL, messages=messages_for_unit_test(python), stream=True)\n", + " reply = \"\"\n", + " for chunk in stream:\n", + " fragment = chunk.choices[0].delta.content or \"\"\n", + " reply += fragment\n", + " yield reply" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8669f56b-8314-4582-a167-78842caea131", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_claude(python):\n", + " result = claude.messages.stream(\n", + " model=CLAUDE_MODEL,\n", + " max_tokens=2000,\n", + " system=system_message,\n", + " messages=[{\"role\": \"user\", \"content\": user_prompt_for_unit_test(python)}],\n", + " )\n", + " reply = \"\"\n", + " with result as stream:\n", + " for text in stream.text_stream:\n", + " reply += text\n", + " yield reply" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97205162", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_gemini(python):\n", + " response = client.models.generate_content_stream(\n", + " model=GEMINI_MODEL,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_message),\n", + " contents=user_prompt_for_unit_test(python)\n", + " )\n", + "\n", + " reply = \"\"\n", + " for chunk in response:\n", + " fragment = chunk.text or \"\"\n", + " reply += fragment\n", + " yield reply\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f94b13e", + "metadata": {}, + "outputs": [], + "source": [ + "def stream_llama_local(python):\n", + " stream = ollama.chat(\n", + " model='llama3.2',\n", + " messages=messages_for_unit_test(python),\n", + " stream=True,\n", + " )\n", + "\n", + " reply = \"\"\n", + " # Iterate through the streamed chunks and print the content\n", + " for chunk in stream:\n", + " #print(chunk['message']['content'], end='', flush=True)\n", + " if 'content' in chunk['message']:\n", + " fragment = chunk['message']['content']\n", + " reply += fragment\n", + " yield reply\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f1ae8f5-16c8-40a0-aa18-63b617df078d", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_unit_test(python, model):\n", + " if model==\"GPT\":\n", + " result = stream_gpt(python)\n", + " elif model==\"Claude\":\n", + " result = stream_claude(python)\n", + " elif model==\"Gemini\":\n", + " result = stream_gemini(python)\n", + " elif model==\"Llama\":\n", + " result = stream_llama_local(python)\n", + "\n", + " else:\n", + " raise ValueError(\"Unknown model\")\n", + " for stream_so_far in result:\n", + " yield stream_so_far" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1ddb38e-6b0a-4c37-baa4-ace0b7de887a", + "metadata": {}, + "outputs": [], + "source": [ + "with gr.Blocks() as ui:\n", + " with gr.Row():\n", + " python = gr.Textbox(label=\"Python code:\", lines=10, value=python_hard)\n", + " unit_test = gr.Textbox(label=\"Unit test\", lines=10)\n", + " with gr.Row():\n", + " model = gr.Dropdown([\"GPT\", \"Claude\", \"Gemini\", \"Llama\"], label=\"Select model\", value=\"GPT\")\n", + " generate_ut = gr.Button(\"Generate Unit tests\")\n", + "\n", + " generate_ut.click(generate_unit_test, inputs=[python, model], outputs=[unit_test])\n", + "\n", + "ui.launch(inbrowser=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week5/community-contributions/DocSearch_Notion/DocSearch_notion_gemini.ipynb b/week5/community-contributions/DocSearch_Notion/DocSearch_notion_gemini.ipynb new file mode 100644 index 0000000..1f233c0 --- /dev/null +++ b/week5/community-contributions/DocSearch_Notion/DocSearch_notion_gemini.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Personal Knowledge Worker\n", + "\n", + "Search through your exported Notion Workspace with Gemini models using RAG.\n", + "\n", + "How to export the content from Notion: https://www.notion.com/help/export-your-content" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports and Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U -q langchain-google-genai" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "import glob\n", + "from dotenv import load_dotenv\n", + "import gradio as gr\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.document_loaders import DirectoryLoader, TextLoader\n", + "from langchain.text_splitter import CharacterTextSplitter\n", + "from langchain.schema import Document\n", + "from langchain_chroma import Chroma\n", + "from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI\n", + "from langchain.memory import ConversationBufferMemory\n", + "from langchain.chains import ConversationalRetrievalChain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "LLM_MODEL = \"gemini-2.5-flash-lite\"\n", + "EMBEDDINGS_MODEL = \"models/gemini-embedding-001\"\n", + "db_name = \"vector_db\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "load_dotenv()\n", + "os.environ['GOOGLE_API_KEY'] = os.getenv('GOOGLE_API_KEY', 'your-key-if-not-using-env')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vector DB Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Clean up and Load Documents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Clean up the Notion directory, remove MD5 hashes from filenames and directory names\n", + "\n", + "# Root directory of your export\n", + "root_dir = \"notion_export\"\n", + "\n", + "# Regex to match the hash: space + 24-32 hex chars (sometimes longer)\n", + "hash_pattern = re.compile(r\"\\s[0-9a-f]{16,32}(_all)?\")\n", + "\n", + "for dirpath, dirnames, filenames in os.walk(root_dir, topdown=False):\n", + " # Rename files\n", + " for filename in filenames:\n", + " new_name = re.sub(hash_pattern, \"\", filename)\n", + " if new_name != filename:\n", + " old_path = os.path.join(dirpath, filename)\n", + " new_path = os.path.join(dirpath, new_name)\n", + " print(f\"Renaming file: {old_path} -> {new_path}\")\n", + " os.rename(old_path, new_path)\n", + "\n", + " # Rename directories\n", + " for dirname in dirnames:\n", + " new_name = re.sub(hash_pattern, \"\", dirname)\n", + " if new_name != dirname:\n", + " old_path = os.path.join(dirpath, dirname)\n", + " new_path = os.path.join(dirpath, new_name)\n", + " print(f\"Renaming dir: {old_path} -> {new_path}\")\n", + " os.rename(old_path, new_path)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Read in documents using LangChain's loaders\n", + "\n", + "documents = []\n", + "for dirpath, dirnames, filenames in os.walk(root_dir):\n", + " # Define doc_type relative to root_dir\n", + " doc_type = os.path.relpath(dirpath, root_dir)\n", + "\n", + " # for main pages in Notion\n", + " if doc_type == \".\":\n", + " doc_type = \"Main\"\n", + " \n", + " loader = DirectoryLoader(\n", + " dirpath,\n", + " glob=\"**/*.md\", # recursive match inside dirpath\n", + " loader_cls=TextLoader\n", + " )\n", + " \n", + " folder_docs = loader.load()\n", + " for doc in folder_docs:\n", + " doc.metadata[\"doc_type\"] = doc_type\n", + " documents.append(doc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create chunks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "text_splitter = CharacterTextSplitter(chunk_size=800, chunk_overlap=200)\n", + "chunks = text_splitter.split_documents(documents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(chunks)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "doc_types = set(chunk.metadata['doc_type'] for chunk in chunks)\n", + "print(f\"Document types found: {', '.join(doc_types)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "embeddings = GoogleGenerativeAIEmbeddings(model=EMBEDDINGS_MODEL)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# If you don't want to recreate the collection\n", + "\n", + "vectorstore = Chroma(embedding_function=embeddings, persist_directory=db_name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check if a Chroma Datastore already exists - if so, delete the collection to start from scratch\n", + "\n", + "if os.path.exists(db_name):\n", + " Chroma(persist_directory=db_name, embedding_function=embeddings).delete_collection()\n", + "\n", + "# Create our Chroma vectorstore!\n", + "\n", + "vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, persist_directory=db_name)\n", + "print(f\"Vectorstore created with {vectorstore._collection.count()} documents\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get one vector and find how many dimensions it has\n", + "\n", + "collection = vectorstore._collection\n", + "sample_embedding = collection.get(limit=1, include=[\"embeddings\"])[\"embeddings\"][0]\n", + "dimensions = len(sample_embedding)\n", + "print(f\"The vectors have {dimensions:,} dimensions\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## RAG pipeline using LangChain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# create a new Chat with ChatGoogleGenerativeAI\n", + "llm = ChatGoogleGenerativeAI(model=LLM_MODEL, temperature=0.7)\n", + "\n", + "# set up the conversation memory for the chat\n", + "memory = ConversationBufferMemory(memory_key='chat_history', return_messages=True)\n", + "\n", + "# the retriever is an abstraction over the VectorStore that will be used during RAG\n", + "retriever = vectorstore.as_retriever()\n", + "\n", + "# putting it together: set up the conversation chain with the GPT 4o-mini LLM, the vector store and memory\n", + "conversation_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever, memory=memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Gradio User Interface" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def chat(message, history):\n", + " result = conversation_chain.invoke({\"question\": message})\n", + " return result[\"answer\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "view = gr.ChatInterface(chat, type=\"messages\").launch(inbrowser=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/week5/community-contributions/linkedin-ai-assistant/Complete_LinkedInDataExport.zip.zip b/week5/community-contributions/linkedin-ai-assistant/Complete_LinkedInDataExport.zip.zip new file mode 100644 index 0000000..ff9ec05 Binary files /dev/null and b/week5/community-contributions/linkedin-ai-assistant/Complete_LinkedInDataExport.zip.zip differ diff --git a/week5/community-contributions/linkedin-ai-assistant/README.md b/week5/community-contributions/linkedin-ai-assistant/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/week5/community-contributions/linkedin-ai-assistant/README.md @@ -0,0 +1 @@ + diff --git a/week5/community-contributions/linkedin-ai-assistant/app.py b/week5/community-contributions/linkedin-ai-assistant/app.py new file mode 100644 index 0000000..6e3ed3f --- /dev/null +++ b/week5/community-contributions/linkedin-ai-assistant/app.py @@ -0,0 +1,1143 @@ +import os +import json +import pandas as pd +import numpy as np +from pathlib import Path +import re +from datetime import datetime, timedelta +from bs4 import BeautifulSoup +import html2text +from collections import Counter, defaultdict, deque +import warnings +import time +import hashlib +import socket +import random +import zipfile +import tempfile +import shutil + +warnings.filterwarnings('ignore') + +import gradio as gr +import chromadb +from sentence_transformers import SentenceTransformer +from transformers import AutoModelForSequenceClassification, AutoTokenizer +from openai import OpenAI +import torch + +# ================================ +# USAGE PROTECTION SYSTEM +# ================================ + +class UsageTracker: + def __init__(self): + self.hourly_limits = defaultdict(lambda: deque()) + self.daily_limits = defaultdict(int) + self.total_requests = 0 + self.total_cost = 0.0 + + # STRICTER LIMITS for cost control + self.max_hourly = 5 # Reduced from 15 + self.max_daily = 20 # Reduced from 100 + self.max_total = 200 # Reduced from 1000 + self.max_daily_cost = 3.0 # $3 daily limit + + # GPT-4o-mini pricing (approximate cost per request) + self.cost_per_request = 0.01 # ~1 cent per request (conservative estimate) + + def can_make_request(self, user_id): + now = datetime.now() + hour_ago = now - timedelta(hours=1) + + # Clean old hourly requests + while self.hourly_limits[user_id] and self.hourly_limits[user_id][0] < hour_ago: + self.hourly_limits[user_id].popleft() + + # Check limits + if len(self.hourly_limits[user_id]) >= self.max_hourly: + return False, f"⏰ Hourly limit reached ({self.max_hourly} requests/hour). Please try again in a few minutes." + + if self.daily_limits[user_id] >= self.max_daily: + return False, f"📅 Daily limit reached ({self.max_daily} requests/day). Come back tomorrow!" + + if self.total_requests >= self.max_total: + return False, "🚫 Service temporarily unavailable due to high usage. Please try again later." + + # Check estimated daily cost + if self.total_cost >= self.max_daily_cost: + return False, f"💰 Daily cost limit (${self.max_daily_cost}) reached. Service will reset tomorrow." + + return True, "OK" + + def record_request(self, user_id): + now = datetime.now() + self.hourly_limits[user_id].append(now) + self.daily_limits[user_id] += 1 + self.total_requests += 1 + self.total_cost += self.cost_per_request # Track estimated cost + + def get_usage_info(self): + """Get current usage info for display""" + return f""" +**📊 Current Usage:** +- Total requests today: {self.total_requests}/{self.max_total} +- Estimated cost today: ${self.total_cost:.2f}/${self.max_daily_cost} +- Service status: {'🟢 Active' if self.total_requests < self.max_total and self.total_cost < self.max_daily_cost else '🔴 Limited'} +""" + +# Initialize tracker - ADD THIS LINE! +usage_tracker = UsageTracker() + + +def protected_function(func): + def wrapper(*args, **kwargs): + user_id = hashlib.md5(str(time.time()).encode()).hexdigest()[:8] + allowed, message = usage_tracker.can_make_request(user_id) + + if not allowed: + return f"⚠️ {message}. Please try again later." + + usage_tracker.record_request(user_id) + return func(*args, **kwargs) + return wrapper + +# ================================ +# LINKEDIN DATA PROCESSOR +# ================================ + +class LinkedInDataProcessor: + def __init__(self, data_path): + self.data_path = Path(data_path) + self.profile_data = {} + self.processed_data = {} + self.articles_content = [] + self.rag_documents = [] + + def load_all_data(self): + """Load all LinkedIn JSON and CSV files including HTML articles""" + print("🔄 Loading LinkedIn data...") + + file_mappings = { + 'Profile.csv': 'basic_info', + 'Connections.csv': 'connections', + 'Experience.csv': 'experience', + 'Education.csv': 'education', + 'Skills.csv': 'skills', + 'Certifications.csv': 'certifications', + 'Articles.csv': 'articles_metadata', + 'Comments.csv': 'comments', + 'Shares.csv': 'shares', + 'Positions.csv': 'positions', + 'Languages.csv': 'languages', + 'Projects.csv': 'projects', + 'Publications.csv': 'publications', + 'Recommendations.csv': 'recommendations', + 'Endorsement_Given_Info.csv': 'endorsements_given', + 'Endorsement_Received_Info.csv': 'endorsements_received', + 'Courses.csv': 'courses', + 'Learning.csv': 'learning_paths', + 'Interests.csv': 'interests', + 'Company Follow.csv': 'companies_followed', + 'Reactions.csv': 'reactions', + 'Views.csv': 'views', + 'Saved_Items.csv': 'saved_items', + } + + loaded_count = 0 + for file_name, data_type in file_mappings.items(): + file_path = self.data_path / file_name + if file_path.exists(): + try: + df = pd.read_csv(file_path, encoding='utf-8') + self.profile_data[data_type] = df + print(f"✅ Loaded {file_name}: {len(df)} records") + loaded_count += 1 + except Exception as e: + print(f"⚠️ Could not load {file_name}: {str(e)}") + else: + print(f"📁 {file_name} not found") + + self.load_html_articles() + print(f"🎉 Successfully loaded {loaded_count} data files") + return loaded_count > 0 + + def load_html_articles(self): + """Load and parse HTML articles""" + print("\n📰 Loading HTML articles...") + + articles_paths = [ + self.data_path / "Articles" / "Articles", + self.data_path / "Articles", + self.data_path / "articles" / "articles", + self.data_path / "articles", + ] + + found_path = None + for path in articles_paths: + if path.exists(): + found_path = path + break + + if not found_path: + print("📁 Articles folder not found") + return + + html_files = list(found_path.glob("*.html")) + if not html_files: + print("📄 No HTML files found") + return + + print(f"📄 Found {len(html_files)} HTML articles") + + articles_data = [] + for html_file in html_files: + try: + article_data = self.parse_html_article(html_file) + if article_data: + articles_data.append(article_data) + except Exception as e: + print(f"⚠️ Error parsing {html_file.name}: {str(e)}") + + self.articles_content = articles_data + self.profile_data['articles_html'] = articles_data + print(f"🎉 Successfully loaded {len(articles_data)} articles") + + def extract_linkedin_url_from_html(self, html_content, filename): + """Extract LinkedIn URL from HTML article content""" + try: + soup = BeautifulSoup(html_content, 'html.parser') + + # Look for canonical URL + canonical = soup.find('link', {'rel': 'canonical'}) + if canonical and canonical.get('href'): + url = canonical.get('href') + if 'linkedin.com' in url: + return url + + # Look for meta property og:url + og_url = soup.find('meta', {'property': 'og:url'}) + if og_url and og_url.get('content'): + url = og_url.get('content') + if 'linkedin.com' in url: + return url + + # Look for any LinkedIn URLs in the content + linkedin_pattern = r'https?://(?:www\.)?linkedin\.com/pulse/[^"\s<>]+' + matches = re.findall(linkedin_pattern, html_content) + if matches: + return matches[0] + + # Fallback: construct URL from filename + if filename: + clean_name = re.sub(r'^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+-', '', filename) + clean_name = clean_name.replace('.html', '') + + if len(clean_name) > 10 and '-' in clean_name: + return f"https://www.linkedin.com/pulse/{clean_name}/" + + return None + + except Exception as e: + print(f"Error extracting LinkedIn URL: {e}") + return None + + def parse_html_article(self, file_path): + """Parse individual HTML article with LinkedIn URL extraction""" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + soup = BeautifulSoup(content, 'html.parser') + + # Extract title + title_elem = soup.find('h1') or soup.find('title') + title = title_elem.get_text().strip() if title_elem else self.extract_title_from_filename(file_path.name) + + # Extract LinkedIn URL + linkedin_url = self.extract_linkedin_url_from_html(content, file_path.name) + + # Extract content + content_selectors = ['article', '.article-content', '.post-content', 'main', '.content', 'body'] + article_content = None + for selector in content_selectors: + article_content = soup.select_one(selector) + if article_content: + break + + if not article_content: + article_content = soup.find('body') or soup + + # Convert to plain text + h = html2text.HTML2Text() + h.ignore_links = True + h.ignore_images = True + plain_text = h.handle(str(article_content)).strip() + + # Extract metadata + words = re.findall(r'\b\w+\b', plain_text.lower()) + + return { + 'filename': file_path.name, + 'title': title, + 'content': str(article_content), + 'plain_text': plain_text, + 'date_published': self.extract_date_from_filename(file_path.name), + 'word_count': len(words), + 'topics': self.extract_topics(plain_text), + 'writing_style': self.analyze_writing_style(plain_text), + 'linkedin_url': linkedin_url + } + + def extract_title_from_filename(self, filename): + """Extract readable title from filename""" + title = filename.replace('.html', '') + title = re.sub(r'^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+-', '', title) + title = title.replace('-', ' ').replace('_', ' ') + return ' '.join(word.capitalize() for word in title.split()) + + def extract_date_from_filename(self, filename): + """Extract publication date from filename""" + date_match = re.search(r'(\d{4}-\d{2}-\d{2})', filename) + return date_match.group(1) if date_match else '' + + def analyze_writing_style(self, text): + """Analyze writing style indicators""" + text_lower = text.lower() + sentences = re.split(r'[.!?]+', text) + words = re.findall(r'\b\w+\b', text_lower) + + return { + 'word_count': len(words), + 'sentence_count': len(sentences), + 'avg_sentence_length': len(words) / max(len(sentences), 1), + 'question_count': text.count('?'), + 'first_person_usage': len(re.findall(r'\b(i|me|my|myself|we|us|our)\b', text_lower)), + 'technical_terms': sum(text_lower.count(term) for term in ['algorithm', 'framework', 'methodology', 'data', 'analysis', 'technology']), + } + + def extract_topics(self, text, max_topics=10): + """Extract main topics from text""" + words = re.findall(r'\b[a-zA-Z]{3,}\b', text.lower()) + stop_words = {'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'been', 'have', 'has', 'had'} + word_freq = Counter(word for word in words if word not in stop_words and len(word) > 3) + return [word for word, count in word_freq.most_common(max_topics)] + + def create_rag_documents(self): + """Create documents for RAG system with LinkedIn URLs""" + self.rag_documents = [] + + # Process profile data + for data_type, data_content in self.profile_data.items(): + if isinstance(data_content, pd.DataFrame) and not data_content.empty: + self.process_dataframe_to_documents(data_content, data_type) + elif isinstance(data_content, list) and data_content: + self.process_list_to_documents(data_content, data_type) + + # Process articles with LinkedIn URLs + if self.articles_content: + for article in self.articles_content: + if article['plain_text'].strip(): + self.rag_documents.append({ + 'text': article['plain_text'], + 'title': article['title'], + 'source_type': 'article', + 'date_published': article['date_published'], + 'word_count': article['word_count'], + 'topics': article['topics'], + 'linkedin_url': article.get('linkedin_url', ''), + 'filename': article['filename'] + }) + + print(f"📚 Created {len(self.rag_documents)} RAG documents with LinkedIn URLs") + return self.rag_documents + + def process_dataframe_to_documents(self, df, data_type): + """Convert DataFrame to RAG documents""" + if data_type == 'experience': + for _, row in df.iterrows(): + text = f"Experience: {row.get('Title', '')} at {row.get('Company', '')}\n" + text += f"Duration: {row.get('Started On', '')} - {row.get('Finished On', 'Present')}\n" + text += f"Description: {row.get('Description', '')}" + + self.rag_documents.append({ + 'text': text, + 'title': f"{row.get('Title', '')} at {row.get('Company', '')}", + 'source_type': 'experience', + 'linkedin_url': '' + }) + + elif data_type == 'education': + for _, row in df.iterrows(): + text = f"Education: {row.get('Degree', '')} in {row.get('Field Of Study', '')} from {row.get('School', '')}\n" + text += f"Duration: {row.get('Start Date', '')} - {row.get('End Date', '')}" + + self.rag_documents.append({ + 'text': text, + 'title': f"{row.get('Degree', '')} - {row.get('School', '')}", + 'source_type': 'education', + 'linkedin_url': '' + }) + + elif data_type == 'skills': + if 'Skill' in df.columns: + skills_text = "Professional Skills: " + ", ".join(df['Skill'].dropna().tolist()) + self.rag_documents.append({ + 'text': skills_text, + 'title': 'Professional Skills', + 'source_type': 'skills', + 'linkedin_url': '' + }) + + elif data_type == 'certifications': + if 'Name' in df.columns: + certs_text = "Certifications: " + ", ".join(df['Name'].dropna().tolist()) + self.rag_documents.append({ + 'text': certs_text, + 'title': 'Certifications', + 'source_type': 'certifications', + 'linkedin_url': '' + }) + + elif data_type == 'projects': + for _, row in df.iterrows(): + text = f"Project: {row.get('Title', '')}\n" + text += f"Description: {row.get('Description', '')}\n" + text += f"URL: {row.get('Url', '')}" + + project_url = row.get('Url', '') + linkedin_url = project_url if 'linkedin.com' in str(project_url) else '' + + self.rag_documents.append({ + 'text': text, + 'title': row.get('Title', 'Project'), + 'source_type': 'projects', + 'linkedin_url': linkedin_url + }) + + def process_list_to_documents(self, data_list, data_type): + """Convert list data to RAG documents""" + if data_type == 'articles_html': + return + + def get_profile_summary(self): + """Get comprehensive profile summary""" + summary = { + 'total_documents': len(self.rag_documents), + 'articles_count': len(self.articles_content), + 'data_types': list(self.profile_data.keys()), + 'skills_count': len(self.profile_data.get('skills', [])), + 'experience_count': len(self.profile_data.get('experience', [])), + 'education_count': len(self.profile_data.get('education', [])), + } + + if self.articles_content: + total_words = sum(article['word_count'] for article in self.articles_content) + summary['total_article_words'] = total_words + summary['avg_article_length'] = total_words // len(self.articles_content) + + return summary + +# ================================ +# RAG SYSTEM +# ================================ + +class LinkedInRAGSystem: + def __init__(self, chroma_db_path): + self.chroma_db_path = chroma_db_path + self.embedding_model = None + self.cross_encoder_model = None + self.cross_encoder_tokenizer = None + self.chroma_client = None + self.collection = None + self.openai_client = None + + def initialize_models(self): + """Initialize all required models""" + print("🔄 Initializing RAG models...") + + # Initialize OpenAI client + try: + api_key = os.getenv('OPENAI_API_KEY') + if not api_key: + print("❌ OpenAI API key not found in environment variables") + return False + self.openai_client = OpenAI(api_key=api_key) + print("✅ OpenAI client initialized") + except Exception as e: + print(f"❌ Failed to initialize OpenAI client: {e}") + return False + + # Load embedding model + try: + self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2') + print("✅ Embedding model loaded") + except Exception as e: + print(f"❌ Failed to load embedding model: {e}") + return False + + # Load cross-encoder for reranking + try: + cross_encoder_name = "cross-encoder/ms-marco-MiniLM-L-6-v2" + self.cross_encoder_tokenizer = AutoTokenizer.from_pretrained(cross_encoder_name) + self.cross_encoder_model = AutoModelForSequenceClassification.from_pretrained(cross_encoder_name) + print("✅ Cross-encoder model loaded") + except Exception as e: + print(f"❌ Failed to load cross-encoder: {e}") + return False + + # Initialize ChromaDB + try: + self.chroma_client = chromadb.PersistentClient(path=self.chroma_db_path) + print("✅ ChromaDB initialized") + except Exception as e: + print(f"❌ Failed to initialize ChromaDB: {e}") + return False + + return True + + def create_vector_store(self, documents): + """Create vector store from documents with enhanced metadata""" + print("🔄 Creating vector store with LinkedIn URLs...") + + # Delete existing collection if it exists + try: + self.chroma_client.delete_collection("linkedin_profile") + except: + pass + + # Create new collection + self.collection = self.chroma_client.create_collection("linkedin_profile") + + # Generate embeddings + texts = [doc['text'] for doc in documents] + embeddings = self.embedding_model.encode(texts, show_progress_bar=True) + + # Prepare data for ChromaDB with enhanced metadata + ids = [f"doc_{i}" for i in range(len(documents))] + metadatas = [] + + for doc in documents: + metadata = {} + for k, v in doc.items(): + if k != 'text': + if k == 'linkedin_url' and v: + metadata[k] = str(v) + elif k == 'date_published' and v: + metadata[k] = str(v) + elif k == 'topics' and isinstance(v, list): + metadata[k] = ', '.join(v) if v else '' + elif v is not None: + metadata[k] = str(v) + else: + metadata[k] = '' + metadatas.append(metadata) + + # Add to collection + batch_size = 100 + for i in range(0, len(texts), batch_size): + end_idx = min(i + batch_size, len(texts)) + self.collection.add( + embeddings=embeddings[i:end_idx].tolist(), + documents=texts[i:end_idx], + metadatas=metadatas[i:end_idx], + ids=ids[i:end_idx] + ) + + print(f"✅ Vector store created with {self.collection.count()} documents") + return True + + def retrieve_and_rerank(self, query, initial_k=20, final_n=5): + """Retrieve and rerank documents""" + if not self.collection: + return [] + + try: + # Initial retrieval + query_embedding = self.embedding_model.encode(query).tolist() + results = self.collection.query( + query_embeddings=[query_embedding], + n_results=initial_k, + include=['documents', 'metadatas'] + ) + + if not results['documents'][0]: + return [] + + # Prepare for reranking + documents = results['documents'][0] + metadatas = results['metadatas'][0] + + # Rerank with cross-encoder + pairs = [[query, doc] for doc in documents] + inputs = self.cross_encoder_tokenizer( + pairs, + padding=True, + truncation=True, + return_tensors='pt', + max_length=512 + ) + + with torch.no_grad(): + scores = self.cross_encoder_model(**inputs).logits.squeeze() + + if scores.dim() == 0: + scores = [scores.item()] + else: + scores = scores.tolist() + + # Sort by score + scored_docs = list(zip(documents, metadatas, scores)) + scored_docs.sort(key=lambda x: x[2], reverse=True) + + # Return top documents + return [{'text': doc, 'metadata': meta, 'score': score} + for doc, meta, score in scored_docs[:final_n]] + + except Exception as e: + print(f"Error in retrieve_and_rerank: {e}") + return [] + + def generate_response(self, query, retrieved_docs): + """Generate response using OpenAI""" + if not retrieved_docs: + return "I couldn't find relevant information to answer your question." + + context = "\n\n".join([doc['text'] for doc in retrieved_docs]) + + messages = [ + { + "role": "system", + "content": """You are an AI assistant representing a LinkedIn profile. Answer questions based ONLY on the provided context from the LinkedIn profile data and articles. + +Guidelines: +- Be professional and personable +- Provide specific details when available +- If information isn't in the context, politely say so +- Use first person when appropriate (since you're representing the profile owner) +- Keep responses concise but informative +- Do not mention or reference the sources in your response - that will be handled separately""" + }, + { + "role": "user", + "content": f"Context:\n{context}\n\nQuestion: {query}\n\nPlease answer based on the LinkedIn profile information provided:" + } + ] + + try: + response = self.openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=messages, + max_tokens=400, + temperature=0.3, + top_p=0.9 + ) + return response.choices[0].message.content.strip() + except Exception as e: + return f"Sorry, I encountered an error generating a response: {str(e)}" + + def format_sources_with_links(self, retrieved_docs): + """Format sources with clickable LinkedIn links""" + if not retrieved_docs: + return "" + + sources_html = "

**📚 Sources:**
" + + for i, doc in enumerate(retrieved_docs, 1): + metadata = doc['metadata'] + source_type = metadata.get('source_type', 'Unknown') + title = metadata.get('title', 'Untitled') + linkedin_url = metadata.get('linkedin_url', '') + date_published = metadata.get('date_published', '') + + # Create source entry + if linkedin_url: + # Clickable LinkedIn link + source_entry = f"🔗 {title}" + if date_published: + source_entry += f" ({date_published})" + else: + # No link available + source_entry = f"📄 **{title}**" + if date_published: + source_entry += f" ({date_published})" + + # Add source type badge + type_color = { + 'article': '#0077B5', + 'experience': '#2D7D32', + 'education': '#7B1FA2', + 'skills': '#F57C00', + 'projects': '#D32F2F', + 'certifications': '#1976D2' + }.get(source_type, '#666') + + source_type_badge = f"{source_type.title()}" + + sources_html += f"{i}. {source_entry}{source_type_badge}
" + + return sources_html + + def chat(self, query): + """Main chat function with enhanced source linking""" + retrieved_docs = self.retrieve_and_rerank(query) + response = self.generate_response(query, retrieved_docs) + + # Add formatted sources with links + sources_info = self.format_sources_with_links(retrieved_docs) + + return response + sources_info + +# ================================ +# UTILITY FUNCTIONS +# ================================ + +def extract_uploaded_data(zip_file_path, extract_to): + """Extract uploaded LinkedIn data zip file""" + try: + with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: + zip_ref.extractall(extract_to) + print(f"✅ Extracted data to {extract_to}") + return True + except Exception as e: + print(f"❌ Failed to extract zip file: {e}") + return False + +def initialize_linkedin_chatbot(data_path): + """Initialize the complete LinkedIn chatbot system with clickable sources""" + print("🚀 Initializing LinkedIn Profile Chatbot with clickable sources...") + + # Step 1: Load and process data + processor = LinkedInDataProcessor(data_path) + if not processor.load_all_data(): + return None, "Failed to load LinkedIn data. Please check the uploaded data." + + # Step 2: Create RAG documents with LinkedIn URLs + documents = processor.create_rag_documents() + if not documents: + return None, "No documents created from LinkedIn data." + + # Count articles with LinkedIn URLs + articles_with_urls = sum(1 for doc in documents if doc.get('linkedin_url') and doc.get('source_type') == 'article') + + # Step 3: Initialize RAG system + temp_db_path = tempfile.mkdtemp() + rag_system = LinkedInRAGSystem(temp_db_path) + if not rag_system.initialize_models(): + return None, "Failed to initialize RAG models." + + # Step 4: Create vector store + if not rag_system.create_vector_store(documents): + return None, "Failed to create vector store." + + # Step 5: Get profile summary + summary = processor.get_profile_summary() + + # Create a clean status message + summary_text = f""" +### ✅ **AI Assistant Ready with Clickable Sources!** + +I have successfully analyzed the LinkedIn profile data including **{summary['total_documents']} documents** and **{summary['articles_count']} published articles** ({articles_with_urls} with direct LinkedIn links). + +**💼 What I can help you discover:** +- 🎯 **Professional Journey** - Career progression and experience +- 🛠️ **Skills & Expertise** - Technical and professional capabilities +- 🎓 **Educational Background** - Academic achievements and learning +- 📝 **Published Content** - Articles with direct LinkedIn links +- 🚀 **Projects & Achievements** - Notable work and accomplishments +- 🌐 **Professional Network** - Industry connections and activities + +**🔗 Enhanced Features:** +- **Clickable Sources** - Direct links to LinkedIn articles and content +- **Smart Source Attribution** - See exactly where information comes from +- **Professional Context** - Answers based on real LinkedIn profile data + +**Ready to explore this professional profile!** Ask me anything you'd like to know. +""" + + return rag_system, summary_text + +# ================================ +# GRADIO INTERFACE +# ================================ + +# Global variables +current_rag_system = None +current_status = "Upload your LinkedIn data to get started!" + +# Add this anywhere in your Gradio interface after the status_display +usage_info = gr.Markdown(value=usage_tracker.get_usage_info()) + +def process_upload(zip_file): + """Process uploaded LinkedIn data""" + global current_rag_system, current_status + + if zip_file is None: + return "Please upload a LinkedIn data ZIP file first.", "" + + try: + # Create temporary directory for extraction + temp_dir = tempfile.mkdtemp() + + # Extract the uploaded file + if extract_uploaded_data(zip_file.name, temp_dir): + # Initialize the RAG system + rag_system, status_message = initialize_linkedin_chatbot(temp_dir) + + if rag_system: + current_rag_system = rag_system + current_status = status_message + return status_message, "✅ **Ready to chat!** Ask me anything about the LinkedIn profile." + else: + return f"❌ Failed to initialize: {status_message}", "" + else: + return "❌ Failed to extract uploaded file.", "" + + except Exception as e: + return f"❌ Error processing upload: {str(e)}", "" + +@protected_function +def chat_with_profile(message, history): + """Chat function with protection""" + global current_rag_system + + if current_rag_system is None: + bot_response = "❌ **Please upload your LinkedIn data first using the file upload above.**" + history.append((message, bot_response)) + return history, "" + + if not message.strip(): + bot_response = "👋 Please enter a question about the LinkedIn profile!" + history.append((message, bot_response)) + return history, "" + + try: + bot_response = current_rag_system.chat(message) + history.append((message, bot_response)) + except Exception as e: + bot_response = f"❌ **Error**: {str(e)}" + history.append((message, bot_response)) + + return history, "" + +# Premium CSS +premium_css = """ +/* Import Google Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +/* Main container styling */ +.gradio-container { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +/* Header styling */ +.main-header { + background: linear-gradient(135deg, #0077B5 0%, #00A0DC 50%, #40E0D0 100%); + color: white; + padding: 2rem; + border-radius: 20px; + margin-bottom: 2rem; + text-align: center; + box-shadow: 0 10px 30px rgba(0,119,181,0.3); + border: 1px solid rgba(255,255,255,0.2); + backdrop-filter: blur(10px); +} + +.main-header h1 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +.main-header p { + font-size: 1.2rem; + opacity: 0.95; + font-weight: 400; +} + +/* Status card styling */ +.status-card { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 8px 25px rgba(0,0,0,0.1); + border: 1px solid rgba(0,119,181,0.1); +} + +/* Chat container */ +.chat-container { + background: white; + border-radius: 20px; + padding: 1.5rem; + box-shadow: 0 10px 40px rgba(0,0,0,0.1); + border: 1px solid rgba(0,119,181,0.1); + max-width: 900px; + margin: 0 auto; +} + +/* Upload container */ +.upload-container { + background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 2rem; + border: 2px dashed #0077B5; +} + +/* Button styling */ +.primary-btn { + background: linear-gradient(135deg, #0077B5 0%, #00A0DC 100%); + color: white; + border: none; + border-radius: 12px; + padding: 0.75rem 1.5rem; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0,119,181,0.3); +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0,119,181,0.4); +} + +/* Example buttons */ +.example-btn { + background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); + color: #0077B5; + border: 1px solid #0077B5; + border-radius: 25px; + padding: 0.6rem 1.2rem; + font-weight: 500; + margin: 0.3rem; + transition: all 0.3s ease; + font-size: 0.9rem; +} + +.example-btn:hover { + background: linear-gradient(135deg, #0077B5 0%, #00A0DC 100%); + color: white; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,119,181,0.3); +} + +/* Input styling */ +.input-text { + border: 2px solid #e1e8ed; + border-radius: 12px; + padding: 1rem; + font-size: 1rem; + transition: all 0.3s ease; + background: #f8fafc; +} + +.input-text:focus { + border-color: #0077B5; + box-shadow: 0 0 0 3px rgba(0,119,181,0.1); + background: white; +} + +/* Chatbot styling */ +.chatbot { + border: none; + border-radius: 16px; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.05); +} + +/* Accordion styling */ +.accordion { + background: linear-gradient(135deg, #f8fafc 0%, #e1e8ed 100%); + border-radius: 12px; + border: 1px solid #e1e8ed; +} +""" + +# Create Gradio interface +with gr.Blocks(css=premium_css, title="LinkedIn Profile AI Assistant", theme=gr.themes.Soft()) as interface: + + # Main Header + gr.HTML(""" +
+

🤖 LinkedIn Profile AI Assistant

+

Intelligent insights with clickable sources to original LinkedIn content

+
+ """) + + # Upload Section + with gr.Column(elem_classes=["upload-container"]): + gr.Markdown("### 📁 **Upload Your LinkedIn Data**") + gr.Markdown("Upload your LinkedIn data export ZIP file to get started. [Learn how to export your LinkedIn data](https://www.linkedin.com/help/linkedin/answer/a1339364)") + + with gr.Row(): + upload_file = gr.File( + label="LinkedIn Data ZIP File", + file_types=[".zip"], + type="filepath" + ) + upload_btn = gr.Button( + "🚀 Process Data", + variant="primary", + elem_classes=["primary-btn"] + ) + + # Status Display + status_display = gr.Markdown( + value="📁 **Upload your LinkedIn data ZIP file above to get started!**", + elem_classes=["status-card"] + ) + + chat_status = gr.Markdown( + value="", + elem_classes=["status-card"] + ) + + # Main Chat Interface + with gr.Column(elem_classes=["chat-container"]): + + # Chat Display + chatbot = gr.Chatbot( + label="💬 Professional Profile Assistant", + height=550, + show_copy_button=True, + avatar_images=("👤", "🤖"), + bubble_full_width=False, + elem_classes=["chatbot"] + ) + + # Input Section + with gr.Row(): + with gr.Column(scale=5): + msg = gr.Textbox( + placeholder="Ask about experience, skills, education, articles, or any aspect of the professional profile...", + label="Your Question", + lines=2, + max_lines=4, + elem_classes=["input-text"] + ) + with gr.Column(scale=1, min_width=100): + submit_btn = gr.Button( + "Send 💬", + variant="primary", + size="lg", + elem_classes=["primary-btn"] + ) + + # Quick Action Buttons + with gr.Row(): + clear_btn = gr.Button("🗑️ Clear Chat", variant="secondary", size="sm") + + # Enhanced Examples Section + with gr.Accordion("💡 Example Questions - Click to Try", open=False, elem_classes=["accordion"]) as examples_accordion: + + gr.Markdown("### 🎯 **Professional Experience & Career**") + with gr.Row(): + exp_q1 = gr.Button("What is the professional background?", elem_classes=["example-btn"], size="sm") + exp_q2 = gr.Button("Describe the career progression", elem_classes=["example-btn"], size="sm") + exp_q3 = gr.Button("What are the key achievements?", elem_classes=["example-btn"], size="sm") + + gr.Markdown("### 🛠️ **Skills & Expertise**") + with gr.Row(): + skill_q1 = gr.Button("What skills and expertise are highlighted?", elem_classes=["example-btn"], size="sm") + skill_q2 = gr.Button("What technologies are mentioned?", elem_classes=["example-btn"], size="sm") + skill_q3 = gr.Button("What are the main areas of expertise?", elem_classes=["example-btn"], size="sm") + + gr.Markdown("### 📚 **Education & Learning**") + with gr.Row(): + edu_q1 = gr.Button("Tell me about the educational background", elem_classes=["example-btn"], size="sm") + edu_q2 = gr.Button("What certifications are mentioned?", elem_classes=["example-btn"], size="sm") + edu_q3 = gr.Button("What courses or learning paths are included?", elem_classes=["example-btn"], size="sm") + + gr.Markdown("### 📝 **Articles & Content**") + with gr.Row(): + content_q1 = gr.Button("What articles have been published?", elem_classes=["example-btn"], size="sm") + content_q2 = gr.Button("What topics are covered in the writing?", elem_classes=["example-btn"], size="sm") + content_q3 = gr.Button("What is the writing style like?", elem_classes=["example-btn"], size="sm") + + # Connect example buttons to input + example_questions = [ + (exp_q1, "What is the professional background and experience?"), + (exp_q2, "Describe the career progression and professional journey"), + (exp_q3, "What are the key achievements and accomplishments?"), + (skill_q1, "What skills and expertise are highlighted in the profile?"), + (skill_q2, "What technologies, tools, and platforms are mentioned?"), + (skill_q3, "What are the main areas of expertise and specialization?"), + (edu_q1, "Tell me about the educational background and qualifications"), + (edu_q2, "What certifications and professional credentials are mentioned?"), + (edu_q3, "What courses, training, or learning paths are included?"), + (content_q1, "What articles and content have been published?"), + (content_q2, "What topics and themes are covered in the published writing?"), + (content_q3, "What is the writing style and approach in the articles?") + ] + + for btn, question in example_questions: + btn.click(lambda q=question: q, outputs=msg) + + # About Section + with gr.Accordion("ℹ️ About This AI Assistant", open=False, elem_classes=["accordion"]): + gr.Markdown(""" + ### 🚀 **Advanced AI-Powered Profile Analysis with Clickable Sources** + + This intelligent assistant uses cutting-edge **Retrieval-Augmented Generation (RAG)** technology to provide accurate, contextual answers about LinkedIn profiles with direct links to original content. + + **🔧 Technical Capabilities:** + - **Vector Search**: Semantic similarity matching for relevant information retrieval + - **Cross-Encoder Reranking**: Advanced relevance scoring for precision + - **GPT-4 Generation**: Natural, human-like response generation + - **Multi-Source Integration**: Combines structured data and article content + - **Clickable Sources**: Direct links to original LinkedIn articles and content + + **📊 Data Sources Analyzed:** + - Professional experience and job history + - Educational background and certifications + - Skills, endorsements, and expertise areas + - Published articles and thought leadership content (with clickable links) + - Projects, achievements, and recommendations + - Professional network activities and engagement + + **🔒 Privacy & Security:** + - Only uses uploaded LinkedIn profile data + - No external data access or web browsing + - Responses based solely on uploaded content + - Secure processing with no data retention + + **⚡ Built with:** + - Gradio for the interface + - OpenAI GPT-4 for generation + - ChromaDB for vector storage + - Sentence Transformers for embeddings + - Custom LinkedIn URL extraction + """) + + # Event Handlers + upload_btn.click( + process_upload, + inputs=[upload_file], + outputs=[status_display, chat_status] + ) + + msg.submit(chat_with_profile, inputs=[msg, chatbot], outputs=[chatbot, msg]) + submit_btn.click(chat_with_profile, inputs=[msg, chatbot], outputs=[chatbot, msg]) + clear_btn.click(lambda: [], outputs=chatbot) + + # Add this to your existing event handlers + submit_btn.click( + lambda: usage_tracker.get_usage_info(), + outputs=usage_info, + queue=False + ) + + # Footer + gr.HTML(""" +
+

🤖 LinkedIn Profile AI Assistant | Powered by Advanced RAG Technology with Clickable Sources

+

Built with ❤️ using Gradio, OpenAI GPT-4, ChromaDB, and Custom LinkedIn URL extraction

+
+ """) + +# Launch the interface +# Launch the interface +if __name__ == "__main__": + interface.launch() \ No newline at end of file diff --git a/week5/community-contributions/linkedin-ai-assistant/requirements.txt b/week5/community-contributions/linkedin-ai-assistant/requirements.txt new file mode 100644 index 0000000..9db7d3d --- /dev/null +++ b/week5/community-contributions/linkedin-ai-assistant/requirements.txt @@ -0,0 +1,15 @@ +openai +sentence-transformers +chromadb +gradio +transformers +torch +accelerate +html2text +beautifulsoup4 +pandas +numpy +matplotlib +seaborn +wordcloud +plotly \ No newline at end of file diff --git a/week5/community-contributions/ruby_rag_console_chat_app/.ruby-gemset b/week5/community-contributions/ruby_rag_console_chat_app/.ruby-gemset new file mode 100644 index 0000000..051ffe6 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/.ruby-gemset @@ -0,0 +1 @@ +ruby_rag_test \ No newline at end of file diff --git a/week5/community-contributions/ruby_rag_console_chat_app/Gemfile b/week5/community-contributions/ruby_rag_console_chat_app/Gemfile new file mode 100644 index 0000000..30b9ff9 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem 'ruby-openai' +gem 'chroma-db', github: 'koic/chroma', branch: 'support_chroma_v2_api' # Using fork because original one comes with outdated API +gem 'pry-byebug' +gem 'ruby-next' # to auto-transpile the required files from source on load. Otherwise the code from chroma-db may not work correctly +gem 'pragmatic_segmenter' \ No newline at end of file diff --git a/week5/community-contributions/ruby_rag_console_chat_app/Gemfile.lock b/week5/community-contributions/ruby_rag_console_chat_app/Gemfile.lock new file mode 100644 index 0000000..8cd5f68 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/Gemfile.lock @@ -0,0 +1,86 @@ +GIT + remote: https://github.com/koic/chroma.git + revision: c457cb0541dffeb4386a8d4d47eaaa68608d2561 + branch: support_chroma_v2_api + specs: + chroma-db (0.8.2) + dry-monads (~> 1.6) + ruby-next (~> 1.0, >= 1.0.3) + zeitwerk (~> 2.6.0) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + byebug (12.0.0) + coderay (1.1.3) + concurrent-ruby (1.3.5) + diff-lcs (1.6.2) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-monads (1.9.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + event_stream_parser (1.0.0) + faraday (2.13.4) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (3.4.1) + net-http (>= 0.5.0) + json (2.13.2) + logger (1.7.0) + method_source (1.1.0) + multipart-post (2.4.1) + net-http (0.6.0) + uri + paco (0.2.3) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + pragmatic_segmenter (0.3.24) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + racc (1.8.1) + require-hooks (0.2.2) + ruby-next (1.1.2) + paco (~> 0.2) + require-hooks (~> 0.2) + ruby-next-core (= 1.1.2) + ruby-next-parser (>= 3.4.0.2) + unparser (~> 0.6.0) + ruby-next-core (1.1.2) + ruby-next-parser (3.4.0.2) + parser (>= 3.0.3.1) + ruby-openai (8.1.0) + event_stream_parser (>= 0.3.0, < 2.0.0) + faraday (>= 1) + faraday-multipart (>= 1) + unparser (0.6.15) + diff-lcs (~> 1.3) + parser (>= 3.3.0) + uri (1.0.3) + zeitwerk (2.6.18) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + chroma-db! + pragmatic_segmenter + pry-byebug + ruby-next + ruby-openai + +BUNDLED WITH + 2.6.8 diff --git a/week5/community-contributions/ruby_rag_console_chat_app/README.md b/week5/community-contributions/ruby_rag_console_chat_app/README.md new file mode 100644 index 0000000..ca11a57 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/README.md @@ -0,0 +1,90 @@ +# Local RAG Knowledge Base with Llama 3.2, Chroma, and Ruby +![Diagram](./doc/ruby_rag_diagram.png) + +## Project Overview + +This mini project demonstrates building a local Retrieval-Augmented Generation (RAG) system using the Llama 3.2 language model (via Ollama locally), Chroma vector database, and Ruby. The project includes: + +- Reading and processing a knowledge base consisting of markdown files organized in folders. +- Splitting documents into context-preserving chunks using the `pragmatic_segmenter` gem for sentence-aware chunking. +- Generating semantic embeddings of chunks with Llama 3.2 local model via an OpenAI-compatible API (Ollama). +- Storing embeddings along with metadata and documents into a Chroma vector database collection. +- Performing similarity search in Chroma on user queries to retrieve relevant context. +- Constructing a conversational prompt by combining chat history, retrieved context, and user input. +- Streaming responses from the Llama 3.2 model back to the console for real-time interaction. + +--- + +## What Has Been Done + +- **Folder crawler and document loader:** + Recursively read all markdown files in the `knowledge_base` directory, assigning document type metadata from folder names. + +- **Smart text chunking:** + Integrated the [`pragmatic_segmenter`](https://github.com/diasks2/pragmatic_segmenter) gem to split texts into sentence-safe chunks (~1000 characters) with overlaps to preserve context and avoid cutting sentences unnaturally. + +- **Embeddings generation:** + Leveraged the local Llama 3.2 model via Ollama's OpenAI-compatible streaming API to generate embeddings of all text chunks, enabling efficient semantic search. + +- **Chroma vector store integration:** + Used the `chroma-db` Ruby gem with a locally hosted Chroma server (via Docker Compose) to store embeddings and metadata, and to perform similarity search queries. + +- **Interactive conversational loop:** + Maintained chat history manually as an array of message hashes, combined with relevant retrieved chunks to form prompts fed into the Llama 3.2 model. + +- **Streaming chat responses:** + Implemented real-time streaming of LLM output to the console leveraging the Ruby OpenAI gem streaming feature and Ollama's compatible API. + +--- + +## Tricky Findings & Gotchas + +- **Ruby Next transpilation required for `chroma-db` gem:** + The gem uses modern Ruby features and requires `gem 'ruby-next'` with `require "ruby-next/language/runtime"` loaded early to avoid LoadErrors. + +- **Chroma API version compatibility:** + Different Chroma server versions expose different API versions (`v1` vs `v2`). The `chroma-db` Ruby gem expected v2 endpoints. Using matched versions of Chroma server and the gem, or a forked gem branch with v2 support, was crucial. + +- **Bundler context for scripts:** + Running scripts must be done with `bundle exec` or with `require 'bundler/setup'` to load local gem dependencies correctly (especially forked gems). + +- **Manual management of conversational memory:** + Unlike Python LangChain, no high-level Ruby library exists for conversation memory or RAG chains, so that had to be implemented as arrays of messages, and prompt assembly was manual. + +- **Text chunking with `pragmatic_segmenter`:** + Using sentence segmentation improved context retention significantly over naïve character splitting, but required careful assembly of chunks and overlaps. + +- **Streaming outputs handled via custom block in Ruby OpenAI gem:** + Streaming integration required capturing delta chunks from the streaming API and printing them in realtime, instead of waiting for full response. + +--- + +## Setup Instructions + +### Requirements + +- Ruby 3.2.x +- Bundler +- Docker & Docker Compose +- Ollama installed and running locally with the `llama3.2` model pulled +- Basic terminal shell (macOS, Linux recommended) + +### Steps + +1. **Clone/Fork the repository:** +2. Run `bundle install` to install Ruby dependencies +3. Run `docker compose up -d` to boot up Chroma DB +4. Run `ollama run llama3.2` to boot up Open Source LLM +5. Run `bundle exec ruby seed.rb` to seed Chroma DB with chunks of data from `knowledge_base` folder +6. Run `bundle exec ruby main.rb` to start actual conversation + +### Questions to try on +1. What is the company name? +2. When the company was establised? +3. Which techologies does the company use? +4. Tell me the emplooees` names and their titles? +5. Who knows how to work with IOS? +6. Tell me who was the client for web project? +7. Is the company looking for IOS developer? + +Then you can compare it with the actual knowledge base. \ No newline at end of file diff --git a/week5/community-contributions/ruby_rag_console_chat_app/bin/console b/week5/community-contributions/ruby_rag_console_chat_app/bin/console new file mode 100755 index 0000000..47a7a6e --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/bin/console @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require 'irb' +require 'irb/completion' +require_relative '../initializer' + +IRB.start \ No newline at end of file diff --git a/week5/community-contributions/ruby_rag_console_chat_app/chromadb/.keep b/week5/community-contributions/ruby_rag_console_chat_app/chromadb/.keep new file mode 100644 index 0000000..e69de29 diff --git a/week5/community-contributions/ruby_rag_console_chat_app/docker-compose.yml b/week5/community-contributions/ruby_rag_console_chat_app/docker-compose.yml new file mode 100644 index 0000000..1ad44f4 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/docker-compose.yml @@ -0,0 +1,12 @@ +services: + chromadb: + image: chromadb/chroma:latest + ports: + - "8000:8000" + environment: + - IS_PERSISTENT=TRUE + - PERSIST_DIRECTORY=/chroma/chroma + - ANONYMIZED_TELEMETRY=TRUE + volumes: + - ./chromadb:/chroma/chroma + restart: unless-stopped diff --git a/week5/community-contributions/ruby_rag_console_chat_app/initializer.rb b/week5/community-contributions/ruby_rag_console_chat_app/initializer.rb new file mode 100644 index 0000000..9e9f7e2 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/initializer.rb @@ -0,0 +1,12 @@ +require 'pathname' +require 'pry' +require 'openai' +require "ruby-next/language/runtime" +require 'chroma-db' +require 'logger' +require 'json' + +Chroma.connect_host = "http://localhost:8000" +Chroma.api_version = "v2" +Chroma.logger = Logger.new($stdout) +Chroma.log_level = Chroma::LEVEL_ERROR \ No newline at end of file diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/about.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/about.md new file mode 100644 index 0000000..8f13771 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/about.md @@ -0,0 +1,15 @@ +# About TechNova Solutions + +TechNova Solutions is a technology services company specializing in **web development**, **iOS mobile app development**, and **machine learning solutions**. Founded in 2015, we have grown into a trusted partner for startups, medium-sized businesses, and enterprises worldwide. + +## Mission +To empower businesses with cutting-edge software solutions that combine usability, scalability, and intelligence. + +## Vision +To be a global leader in delivering high-quality digital products that transform industries. + +## Values +- Innovation: Continuously adopting new technologies. +- Collaboration: Building strong partnerships with clients. +- Excellence: Maintaining high standards in every project. +- Growth: Creating opportunities for both employees and clients. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/careers.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/careers.md new file mode 100644 index 0000000..0343958 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/careers.md @@ -0,0 +1,22 @@ +# Careers at TechNova Solutions + +We are always looking for passionate and skilled professionals to join our growing team. Our company culture emphasizes **learning**, **innovation**, and **collaboration**. + +## Current Openings +- **iOS Developer** + - Skills: Swift, SwiftUI, UIKit, experience with App Store deployment. +- **Web Developer** + - Skills: React.js, Node.js, TypeScript, REST API integration. +- **Machine Learning Engineer** + - Skills: Python, TensorFlow/PyTorch, data pipelines, NLP, computer vision. +- **Project Manager** + - Skills: Agile methodologies, client communication, technical understanding. + +## Why Work With Us? +- Remote-friendly with flexible working hours. +- Opportunities to work on international projects. +- Professional development budget for training and conferences. +- Inclusive and diverse work culture. + +## How to Apply +Send your CV and portfolio to **careers@technova.com** diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/overview.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/overview.md new file mode 100644 index 0000000..f18806e --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/company/overview.md @@ -0,0 +1,37 @@ +# Company Overview: TechNova Solutions + +## Industry +Software Development and Consulting + +## Headquarters +Berlin, Germany (with remote teams worldwide) + +## Services +TechNova Solutions provides: +1. **Web Development** + - Full-stack web solutions + - Modern scalable architectures + - E-commerce and SaaS platforms + +2. **iOS App Development** + - Custom mobile applications + - Swift/SwiftUI-based native solutions + - App Store deployment and maintenance + +3. **Machine Learning Development** + - Predictive analytics + - Natural Language Processing (NLP) models + - Computer vision and automation solutions + +## Client Base +- Startups launching MVPs +- Growing SaaS companies +- Enterprises requiring digital transformation + +## Key Achievements +- Delivered over **150 digital projects** across industries. +- Worked with clients in **FinTech, Healthcare, and E-commerce**. +- Recognized as a **Top B2B Tech Firm in 2023** by Clutch. + +## Culture +Our team thrives on **technical challenges** and believes in **continuous improvement**. We foster a work environment where creativity meets execution. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/ios_project.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/ios_project.md new file mode 100644 index 0000000..b5072e5 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/ios_project.md @@ -0,0 +1,34 @@ +# iOS App Development Agreement + +**Parties:** +Client: Acme Innovations LLC +Developer: TechNova Solutions + +## Project Scope +TechNova Solutions will design, develop, and deploy a custom iOS mobile application for Acme Innovations, including user authentication, dashboard, push notifications, and App Store submission. + +## Deliverables +- iOS app (Swift/SwiftUI) for iPhone +- Admin dashboard (web-based) +- User and technical documentation +- 2 post-launch maintenance updates + +## Timeline +- Project kickoff: October 1, 2025 +- Beta delivery: December 1, 2025 +- Final delivery: January 15, 2026 + +## Payment Terms +- 30% upfront +- 40% upon beta delivery +- 30% upon final delivery + +## Intellectual Property +All source code and produced assets are transferred to the client upon final payment. + +## Confidentiality +Both parties will maintain strict confidentiality regarding proprietary information. + +## Signatures +_Client Rep:_ _____________________ +_TechNova Rep:_ ___________________ diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/ml_project.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/ml_project.md new file mode 100644 index 0000000..c8a11ae --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/ml_project.md @@ -0,0 +1,35 @@ +# Machine Learning Project Agreement + +**Parties:** +Client: HealthPlus Analytics +Developer: TechNova Solutions + +## Objective +TechNova Solutions will develop a predictive analytics solution using machine learning models for healthcare data, aimed at reducing patient readmission rates. + +## Statement of Work +- Data preprocessing and anonymization +- Model development (Python, TensorFlow, scikit-learn) +- Evaluation and performance tuning +- Deployment and staff training +- Integration with client’s data system + +## Timeline +- Data delivery: November 1, 2025 +- Prototype model: January 10, 2026 +- Final delivery: February 20, 2026 + +## Payment Schedule +- 25% upon signing +- 50% upon prototype +- 25% upon final acceptance + +## Confidentiality & Data Privacy +Strict data privacy and NDA agreement. No data shared outside of project scope. + +## Intellectual Property +Custom ML models and documentation are transferred to the client at project completion. + +## Signatures +_Client Rep:_ _____________________ +_TechNova Rep:_ ___________________ diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/web_project.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/web_project.md new file mode 100644 index 0000000..3c74095 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/contracts/web_project.md @@ -0,0 +1,35 @@ +# Web Development Services Agreement + +**Parties:** +Client: BlueSky E-Commerce +Developer: TechNova Solutions + +## Project Description +TechNova Solutions will build a responsive e-commerce web platform with integrated payment gateway, product catalog, and analytics dashboard for BlueSky E-Commerce. + +## Scope of Work +- Modern UX/UI design +- Frontend (React.js), backend (Node.js, PostgreSQL) +- Payment integration (Stripe/PayPal) +- Mobile-responsive design +- 3 months maintenance & support + +## Project Phases +1. Specification & wireframing +2. Development & staging +3. Testing & deployment +4. Support period + +## Pricing & Payment +- Total: €25,000 +- 40% upon contract start, 40% after development milestone, 20% after deployment + +## Intellectual Property +All code, documentation, and design assets become property of the client post-final payment. + +## Dispute Resolution +Any disputes to be resolved via arbitration in Berlin, Germany. + +## Signatures +_Client Rep:_ _____________________ +_TechNova Rep:_ ___________________ diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/alex_johnson.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/alex_johnson.md new file mode 100644 index 0000000..41c92f2 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/alex_johnson.md @@ -0,0 +1,54 @@ +# HR Record + +# Alex Johnson + +## Summary +- **Date of Birth**: May 10, 1990 +- **Job Title**: iOS Engineer +- **Location**: Berlin, Germany + +## Career Progression +- **March 2015**: Joined TechNova Solutions as Junior iOS Developer, focused on building and refining mobile features. +- **July 2017**: Promoted to iOS Engineer, led SwiftUI adoption in new projects. +- **November 2019**: Spearheaded the launch of the flagship iOS app with 100k+ downloads. +- **August 2023**: Mentors junior iOS developers and contributes to open-source Swift libraries. + +## Annual Performance History +- **2021**: + - **Performance Rating**: 4.6/5 + - **Key Achievements**: Implemented a new push notification system improving user retention by 15%. + +- **2022**: + - **Performance Rating**: 4.8/5 + - **Key Achievements**: Led successful iOS 16 app updates supporting new platform features. + +- **2023**: + - **Performance Rating**: 4.7/5 + - **Key Achievements**: Reduced app crash rate by 30% with detailed runtime diagnostics tools. + +## Compensation History +- **2021**: + - **Base Salary**: €65,000 + - **Bonus**: €7,000 + +- **2022**: + - **Base Salary**: €70,000 + - **Bonus**: €8,000 + +- **2023**: + - **Base Salary**: €75,000 + - **Bonus**: €10,000 + +## Other HR Notes +- **Training Completed**: + - Advanced SwiftUI Workshop (2022) + - iOS Performance Optimization Course (2023) + +- **Awards**: + - Employee of the Quarter Q4 2022 + +- **Interests**: + - Open-source iOS community, mountain biking. + +- **Feedback from HR**: + - Alex is highly reliable, innovative, and a technical leader on the iOS team. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/emma_clark.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/emma_clark.md new file mode 100644 index 0000000..f430761 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/emma_clark.md @@ -0,0 +1,36 @@ +# HR Record + +# Emma Clark + +## Summary +- **Date of Birth**: December 1, 1994 +- **Job Title**: Junior iOS Developer +- **Location**: Remote (Barcelona, Spain) + +## Career Progression +- **January 2023**: Joined TechNova Solutions as Junior iOS Developer. +- **June 2023**: Assisted in feature development for client app projects. +- **September 2023**: Completed SwiftUI fundamentals training. + +## Annual Performance History +- **2023**: + - **Performance Rating**: 4.3/5 + - **Key Achievements**: Successfully delivered bug fixes that reduced crash reports by 20%. + +## Compensation History +- **2023**: + - **Base Salary**: €45,000 + - **Bonus**: €2,000 + +## Other HR Notes +- **Training Completed**: + - SwiftUI Fundamentals (2023) + +- **Awards**: + - Rookie of the Month (August 2023) + +- **Interests**: + - Mobile UI design, digital painting. + +- **Feedback from HR**: + - Emma is eager to learn and shows promising potential in iOS development. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/li_wang.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/li_wang.md new file mode 100644 index 0000000..f916bd6 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/li_wang.md @@ -0,0 +1,54 @@ +# HR Record + +# Li Wang + +## Summary +- **Date of Birth**: November 3, 1991 +- **Job Title**: Machine Learning Researcher +- **Location**: Berlin, Germany + +## Career Progression +- **September 2016**: Joined as ML Research Intern focusing on NLP models. +- **March 2018**: Promoted to ML Researcher, published 3 papers in AI conferences. +- **July 2021**: Led development of custom ML pipeline for predictive analytics client project. +- **January 2024**: Mentoring new ML engineers and collaborating cross-functionally. + +## Annual Performance History +- **2021**: + - **Performance Rating**: 4.8/5 + - **Key Achievements**: Developed novel transformer model adaptation improving accuracy by 12%. + +- **2022**: + - **Performance Rating**: 4.9/5 + - **Key Achievements**: Successfully delivered a computer vision system automating quality inspection. + +- **2023**: + - **Performance Rating**: 4.9/5 + - **Key Achievements**: Published an internal white paper on explainable AI. + +## Compensation History +- **2021**: + - **Base Salary**: €68,000 + - **Bonus**: €7,500 + +- **2022**: + - **Base Salary**: €75,000 + - **Bonus**: €9,000 + +- **2023**: + - **Base Salary**: €80,000 + - **Bonus**: €11,000 + +## Other HR Notes +- **Training Completed**: + - AI Model Interpretability Workshop (2022) + - TensorFlow Advanced Techniques (2023) + +- **Awards**: + - Best Paper Award, AI Conference 2022 + +- **Interests**: + - AI ethics, chess. + +- **Feedback from HR**: + - Li is a deep thinker and innovator, key to the ML capabilities of TechNova. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/oliver_smith.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/oliver_smith.md new file mode 100644 index 0000000..3fdd870 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/oliver_smith.md @@ -0,0 +1,54 @@ +# HR Record + +# Oliver Smith + +## Summary +- **Date of Birth**: February 14, 1989 +- **Job Title**: Fullstack Engineer +- **Location**: Berlin, Germany + +## Career Progression +- **May 2013**: Joined TechNova as Junior Web Developer. +- **December 2016**: Became Fullstack Engineer handling Node.js backend and iOS frontend integration. +- **September 2019**: Led cross-platform product integration projects. +- **July 2023**: Leads DevOps initiatives for CI/CD pipeline improvements. + +## Annual Performance History +- **2021**: + - **Performance Rating**: 4.5/5 + - **Key Achievements**: Automated deployment pipelines improving release frequency by 25%. + +- **2022**: + - **Performance Rating**: 4.7/5 + - **Key Achievements**: Improved API response times by 35%. + +- **2023**: + - **Performance Rating**: 4.8/5 + - **Key Achievements**: Led migration to containerized microservices architecture. + +## Compensation History +- **2021**: + - **Base Salary**: €70,000 + - **Bonus**: €7,000 + +- **2022**: + - **Base Salary**: €75,000 + - **Bonus**: €8,000 + +- **2023**: + - **Base Salary**: €80,000 + - **Bonus**: €9,000 + +## Other HR Notes +- **Training Completed**: + - Kubernetes & Docker Training (2022) + - Advanced DevOps Practices (2023) + +- **Awards**: + - TechNova Team Player Award 2023 + +- **Interests**: + - Open-source contributor, drone photography. + +- **Feedback from HR**: + - Oliver’s versatile skill set bridges frontend, backend, and infrastructure seamlessly. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/sophia_martinez.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/sophia_martinez.md new file mode 100644 index 0000000..4d4a0bf --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/employees/sophia_martinez.md @@ -0,0 +1,54 @@ +# HR Record + +# Sophia Martinez + +## Summary +- **Date of Birth**: August 22, 1988 +- **Job Title**: Senior Web Developer +- **Location**: Remote (Madrid, Spain) + +## Career Progression +- **January 2014**: Started as Junior Frontend Developer working on React projects. +- **June 2016**: Promoted to Web Developer handling full-stack responsibilities with Node.js backend. +- **May 2019**: Led e-commerce platform redeployment for major client. +- **February 2023**: Became Senior Web Developer and technical team lead. + +## Annual Performance History +- **2021**: + - **Performance Rating**: 4.7/5 + - **Key Achievements**: Spearheaded transition to microservices architecture. + +- **2022**: + - **Performance Rating**: 4.9/5 + - **Key Achievements**: Delivered a complex multi-tenant SaaS platform on time. + +- **2023**: + - **Performance Rating**: 5.0/5 + - **Key Achievements**: Implemented comprehensive CI/CD pipelines improving deployment speed by 40%. + +## Compensation History +- **2021**: + - **Base Salary**: €72,000 + - **Bonus**: €8,500 + +- **2022**: + - **Base Salary**: €78,000 + - **Bonus**: €10,000 + +- **2023**: + - **Base Salary**: €85,000 + - **Bonus**: €12,000 + +## Other HR Notes +- **Training Completed**: + - Kubernetes Administration (2022) + - Advanced React Patterns (2023) + +- **Awards**: + - TechNova Innovation Award 2023 + +- **Interests**: + - Technical blogging, hiking. + +- **Feedback from HR**: + - Sophia is a strategic thinker and a force-multiplier for the web team. diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/ios_app_dev.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/ios_app_dev.md new file mode 100644 index 0000000..95f1cf9 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/ios_app_dev.md @@ -0,0 +1,29 @@ +# iOS App Development Services + +## Overview +TechNova Solutions offers custom native iOS app development using the latest Swift and SwiftUI technologies to build performant, user-friendly applications for iPhone and iPad. + +## Key Features +- Native UI/UX design optimized for iOS platform +- Integration with Apple services: HealthKit, Push Notifications, Siri Shortcuts +- Robust authentication and security features +- App Store submission support and maintenance +- Continuous updates for compatibility with the latest iOS versions + +## Typical Use Cases +- Consumer mobile apps +- Enterprise tools and productivity apps +- E-commerce and retail apps +- Fitness and wellness tracking + +## Technologies +- Swift & SwiftUI +- Combine for reactive programming +- Core Data and CloudKit for persistence +- XCTest for automated testing + +## Benefits +- Faster time-to-market with modern workflows +- High app performance and responsiveness +- Access to native hardware capabilities +- Dedicated support throughout app lifecycle diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/ml_solutions.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/ml_solutions.md new file mode 100644 index 0000000..4e00211 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/ml_solutions.md @@ -0,0 +1,36 @@ +# Machine Learning Solutions + +## Overview +TechNova Solutions delivers custom machine learning models and AI systems to help businesses unlock insights, automate processes, and drive innovation. + +## Key Capabilities +- Predictive analytics and forecasting +- Natural Language Processing (NLP) for text analysis and chatbots +- Computer vision for image processing and quality control +- Recommendation engines and personalization +- Automated data preprocessing and feature engineering + +## Project Workflow +1. Data collection and cleaning +2. Model development and training +3. Evaluation and performance tuning +4. Deployment and integration +5. Ongoing monitoring and updates + +## Typical Applications +- Customer behavior prediction +- Fraud detection +- Medical diagnostics and image analysis +- Automated content moderation +- Supply chain optimization + +## Technologies +- Python, TensorFlow, PyTorch +- Scikit-learn, Pandas, NumPy +- Cloud ML platforms (AWS SageMaker, GCP AI Platform) + +## Benefits +- Data-driven decision making +- Process automation and efficiency +- Enhanced customer experience through personalization +- Competitive advantage via AI innovation diff --git a/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/web_dev_services.md b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/web_dev_services.md new file mode 100644 index 0000000..72c17aa --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/knowledge_base/products/web_dev_services.md @@ -0,0 +1,37 @@ +# Web Development Services + +## Overview +Our full-stack web development solutions provide scalable, responsive, and secure web applications tailored to diverse business needs, from startups to enterprises. + +## Core Services +- Frontend development using React.js, Vue.js, or Angular +- Backend APIs and microservices with Node.js, Express, and PostgreSQL +- E-commerce platform design and integration +- Content management system (CMS) development +- Cloud deployment and DevOps automation + +## Features +- Mobile-first responsive design +- Secure authentication and role-based access +- API integrations with third-party services +- Real-time data synchronization and notifications +- Scalable architecture for high-traffic applications + +## Typical Clients +- SaaS businesses +- Online retailers and marketplaces +- Media and content platforms +- Financial and healthcare organizations + +## Technologies +- JavaScript/TypeScript +- React, Vue, Angular +- Node.js, Express +- PostgreSQL, MongoDB +- Docker, Kubernetes + +## Benefits +- Custom solutions built for your unique business challenges +- Improved user engagement and retention +- Scalable systems ready for growth +- Expert support and maintenance diff --git a/week5/community-contributions/ruby_rag_console_chat_app/main.rb b/week5/community-contributions/ruby_rag_console_chat_app/main.rb new file mode 100644 index 0000000..d7dab37 --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/main.rb @@ -0,0 +1,68 @@ +require_relative 'initializer' + +chat_history = [ + { role: "system", content: "You are helpful assistant. If you do not know the answer, do not make it up, just say you do not know." } +] +openai = OpenAI::Client.new(uri_base: 'http://localhost:11434/v1', access_token: 'ollama') # LLaMa locally + +def get_embeddings(text, client) + response = client.embeddings(parameters: { model: "llama3.2", input: text }) + response["data"].first["embedding"] +end + +def build_prompt(chat_history:, context_chunks:, user_message:) + context_text = context_chunks.join("\n---\n") + + <<~PROMPT + Here is an additional context from our knowledge base: + #{context_text} + + New question from a user: + User: #{user_message} + + Please answer using the context above. + PROMPT +end + +collection_name = "my_collection" +collection = Chroma::Resources::Collection.get(collection_name) + +puts "Welcome to our little experiment!" + +while true + user_message = gets.chomp + + if user_message == 'exit' + puts "Ending the session" + exit 0 + end + + query_embedding = get_embeddings(user_message, openai) + results = collection.query(query_embeddings: [query_embedding], results: 15) + retrieved_chunks = results.map(&:document) + + prompt = build_prompt( + chat_history: chat_history, + context_chunks: retrieved_chunks, + user_message: user_message + ) + chat_history << { role: "user", content: prompt } + + response_message = "" + openai.chat(parameters: { + model: "llama3.2", + messages: chat_history, + stream: proc { |chunk, _bytesize| + delta = chunk.dig("choices", 0, "delta", "content") + next unless delta + print delta + response_message << delta + } + }) + chat_history << { role: "assistant", content: response_message } + puts +end + + + + diff --git a/week5/community-contributions/ruby_rag_console_chat_app/seed.rb b/week5/community-contributions/ruby_rag_console_chat_app/seed.rb new file mode 100644 index 0000000..d509b7d --- /dev/null +++ b/week5/community-contributions/ruby_rag_console_chat_app/seed.rb @@ -0,0 +1,129 @@ +require_relative 'initializer' +require 'pragmatic_segmenter' + +folders = Dir.glob("knowledge_base/*").select { |f| File.directory?(f) } + +documents = [] + +folders.each do |folder| + doc_type = File.basename(folder) + Dir.glob("#{folder}/**/*.md").each do |filepath| + content = File.read(filepath, encoding: 'utf-8') + doc = { + content: content, + path: filepath, + metadata: { "doc_type" => doc_type } + } + documents << doc + end +end + +# chunk_size and chunk_overlap are configurable for getting better results +def split_text(text, chunk_size: 1000, chunk_overlap: 200) + chunks = [] + start = 0 + while start < text.length + finish = [start + chunk_size, text.length].min + chunks << text[start...finish] + break if finish == text.length + start += (chunk_size - chunk_overlap) + end + chunks +end + +def split_text_by_sentence(text, chunk_size: 1500, chunk_overlap: 200) + ps = PragmaticSegmenter::Segmenter.new(text: text) + sentences = ps.segment + chunks = [] + current_chunk = "" + sentences.each do |sentence| + if (current_chunk + sentence).length > chunk_size + chunks << current_chunk.strip + # For overlap, take last N chars from current_chunk (optionally at sentence boundary) + overlap = current_chunk[-chunk_overlap..-1] || "" + current_chunk = overlap + sentence + else + current_chunk += " " unless current_chunk.empty? + current_chunk += sentence + end + end + chunks << current_chunk.strip unless current_chunk.empty? + chunks +end + +chunks = [] + +# documents.each do |doc| +# split_text(doc[:content]).each_with_index do |chunk, idx| +# chunks << { +# content: chunk, +# metadata: doc[:metadata].merge({ "chunk_index" => idx, "path" => doc[:path] }) +# } +# end +# end + +documents.each do |doc| + split_text_by_sentence(doc[:content]).each_with_index do |chunk, idx| + chunks << { + content: chunk, + metadata: doc[:metadata].merge({ "chunk_index" => idx, "path" => doc[:path] }) + } + end +end + +puts "Chucks count: #{chunks.count}" +puts "Document types found: #{chunks.map { _1[:metadata]['doc_type']}.uniq.join(', ') }" + +# 1. Set up OpenAI client (replace with RubyLLM or HTTP if using HuggingFace) +# openai = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY']) # OpenAI API, remotely +openai = OpenAI::Client.new(uri_base: 'http://localhost:11434/v1', access_token: 'ollama') # LLaMa, locally + +# 2. Get embeddings for each chunk +def get_embedding(text, client) + response = client.embeddings(parameters: { model: "llama3.2", input: text }) + response["data"].first["embedding"] +end + +# Check current Chrome server version +version = Chroma::Resources::Database.version +puts version + +collection_name = "my_collection" +collection = begin + Chroma::Resources::Collection.get(collection_name) +rescue Chroma::APIError => e + nil +end + +if collection + puts "Collection already exists" + puts "Do you want to reset it? (y/n)" + answer = gets.chomp + if answer == 'y' + Chroma::Resources::Collection.delete(collection_name) + puts 'Collection deleted' + exit 0 + end +end + +puts "Creating collection - #{collection_name}" +collection = Chroma::Resources::Collection.create(collection_name, { lang: "ruby" }) unless collection + +chunks.each do |chunk| + chunk[:embedding] = get_embedding(chunk[:content], openai) +end + +# 4. Insert into Chroma +embeddings = chunks.each_with_index.map do |chunk, idx| + Chroma::Resources::Embedding.new( + id: "chunk-#{idx}", + embedding: chunk[:embedding], + metadata: chunk[:metadata], + document: chunk[:content] + ) +end + +collection.add(embeddings) +puts "Vectorstore created with #{embeddings.size} documents" + +# Now 'chunks' is an array of hashes with chunk[:] and metadata. diff --git a/week5/community-contributions/w5_excercise.ipynb b/week5/community-contributions/w5_excercise.ipynb new file mode 100644 index 0000000..85430e3 --- /dev/null +++ b/week5/community-contributions/w5_excercise.ipynb @@ -0,0 +1,120 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "25868a51", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "import gradio as gr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "805b6abc", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize OpenAI and constants\n", + "load_dotenv(override=True)\n", + "api_key = os.getenv('OPENAI_API_KEY')\n", + "MODEL = 'gpt-4o-mini'\n", + "openai = OpenAI()\n", + "\n", + "db_name = \"vector_db\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "382f71f2", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.document_loaders import WebBaseLoader\n", + "from langchain.text_splitter import CharacterTextSplitter\n", + "from langchain_openai import OpenAIEmbeddings, ChatOpenAI\n", + "from langchain_chroma import Chroma\n", + "from langchain.memory import ConversationBufferMemory\n", + "from langchain.chains import ConversationalRetrievalChain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e41daa2f", + "metadata": {}, + "outputs": [], + "source": [ + "loader = WebBaseLoader([\"https://en.wikipedia.org/wiki/Solar_System\",\n", + " \"https://en.wikipedia.org/wiki/Star_Wars_(film)\"])\n", + "docs = loader.load()\n", + "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)\n", + "chunks = text_splitter.split_documents(docs)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a21e1776", + "metadata": {}, + "outputs": [], + "source": [ + "embeddings = OpenAIEmbeddings()\n", + "\n", + "if os.path.exists(db_name):\n", + " Chroma(persist_directory=db_name, embedding_function=embeddings).delete_collection()\n", + "\n", + "vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, persist_directory=db_name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb072ee8", + "metadata": {}, + "outputs": [], + "source": [ + "# create a new Chat with OpenAI\n", + "llm = ChatOpenAI(temperature=0.7, model_name=MODEL)\n", + "memory = ConversationBufferMemory(memory_key='chat_history', return_messages=True)\n", + "retriever = vectorstore.as_retriever()\n", + "conversation_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever, memory=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0531ff73", + "metadata": {}, + "outputs": [], + "source": [ + "def chat(question, history):\n", + " result = conversation_chain.invoke({\"question\": question})\n", + " return result[\"answer\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a87fe151", + "metadata": {}, + "outputs": [], + "source": [ + "view = gr.ChatInterface(chat, type=\"messages\").launch(inbrowser=True)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}