From 1a90283144e3ec46d637a384182ec00ef57528ca Mon Sep 17 00:00:00 2001 From: Maria Bader Date: Tue, 18 Jun 2024 09:18:53 +0200 Subject: [PATCH 1/4] workshop files --- .gitignore | 129 +---- 1_workshop_tools.ipynb | 433 ++++++++++++++ 2_workshop_agents.ipynb | 339 +++++++++++ 3_workshop_advanced.ipynb | 278 +++++++++ README.md | 40 +- helper_functions/helper_functions.py | 19 + helper_functions/keys.py | 9 + helper_functions/tools.py | 127 ++++ images/image_Amsterdam.jpg | Bin 0 -> 34975 bytes images/image_cat_in_a_blue_box.jpg | Bin 0 -> 20089 bytes requirements.txt | 6 + ...image_17th-century_canals_of_Amsterdam.jpg | Bin 0 -> 32302 bytes solution/requirements.txt | 6 + solution/solution_advanced.ipynb | 368 ++++++++++++ solution/solution_agents.ipynb | 543 ++++++++++++++++++ solution/solution_tools.ipynb | 353 ++++++++++++ solutions/Solutions_to_your_workshop.ipynb | 6 - 17 files changed, 2512 insertions(+), 144 deletions(-) create mode 100644 1_workshop_tools.ipynb create mode 100644 2_workshop_agents.ipynb create mode 100644 3_workshop_advanced.ipynb create mode 100644 helper_functions/helper_functions.py create mode 100644 helper_functions/keys.py create mode 100644 helper_functions/tools.py create mode 100644 images/image_Amsterdam.jpg create mode 100644 images/image_cat_in_a_blue_box.jpg create mode 100644 requirements.txt create mode 100644 solution/images/image_17th-century_canals_of_Amsterdam.jpg create mode 100644 solution/requirements.txt create mode 100644 solution/solution_advanced.ipynb create mode 100644 solution/solution_agents.ipynb create mode 100644 solution/solution_tools.ipynb delete mode 100644 solutions/Solutions_to_your_workshop.ipynb diff --git a/.gitignore b/.gitignore index b6e4761..93526df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,129 +1,2 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ +__pycache__/ diff --git a/1_workshop_tools.ipynb b/1_workshop_tools.ipynb new file mode 100644 index 0000000..c1565a9 --- /dev/null +++ b/1_workshop_tools.ipynb @@ -0,0 +1,433 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction to LLM Agents with LangChain\n", + "\n", + "## Notebook 1: Tools\n", + "---\n", + "\n", + "Welcome to the workshop on building LLM agents with LangChain!\n", + "\n", + "**The goal:** \n", + "\n", + "With this notebook you will familiarize yourself with the key concepts of Tools as building blocks of an LLM Agent. At the end, you will have all the code you need to use custom LangChain tools as well as build your own custom tools.\n", + "\n", + "🌟 So ... let us begin! " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**The use case:** \n", + "\n", + "Summer holidays are coming up and you still don't know where to go. Oh no! \n", + "\n", + "You decide to build a tool that helps you get information on holiday locations. For example, you would like to to find out how big a specific city is, what sights are there to see, how the weather there is, and you would like to get a drawing of that place, to get a first impression. Because who does not like art? \n", + "\n", + "You will implement this through a sentinent LLM agent, who has access to\n", + "* the wikipedia API,\n", + "* a weather API,\n", + "* can generate images by using a HuggingFace API. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Content:**\n", + "\n", + "1. [Default LangChain tools](#1)\n", + "
    \n", + "
  1. [Exercise 1 (a): Explore tool parameters](#1a)
  2. \n", + "
  3. [Exercise 1 (b): Run tool and explore output](#1b)
  4. \n", + "
\n", + "2. [Custom tools](#2)\n", + "
    \n", + "
  1. [Exercise 2 (a): Build your own Weather tool](#2a)
  2. \n", + "
  3. [Exercise 2 (b): Build your own Image tool](#2b)
  4. \n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Reminder:** Make sure to update `helper_functions/keys.py` based on keys in [privatebin](https://privatebin.molops.io/?a6459e88fa282c28#DsFZvkZSiuPcNQzNXvmtvmTozihhaf1hQdqBCd7r3q5s)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from helper_functions.keys import WEATHER_KEY, HUGGING_FACE_KEY\n", + "from langchain_community.tools import WikipediaQueryRun\n", + "from langchain_community.utilities import WikipediaAPIWrapper\n", + "from langchain.pydantic_v1 import BaseModel, Field\n", + "from langchain.tools import StructuredTool\n", + "from PIL import Image\n", + "import io" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A Langchain Default tool\n", + "### Default [Wikipedia tool](https://python.langchain.com/v0.1/docs/integrations/tools/wikipedia/) \n", + "\n", + "The cell below loads the full wikipedia tool. It makes an API call to Wikipedia using the ``WikipediaAPIWrapper`` and returns a summary of the queried article. ``WikipediaQueryRun`` then wraps this into a ready made tool. \n", + "\n", + "Each tool is a ``BaseTool`` class object, you can find its definition [here](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.BaseTool.html#langchain_core.tools.BaseTool)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "api_wrapper = WikipediaAPIWrapper(top_k_results=1)\n", + "wiki_tool = WikipediaQueryRun(api_wrapper=api_wrapper)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1 (a): Explore tool parameters \n", + "\n", + "**TASK:** \n", + "Use the methods ``name``, ``description``, ``args``, ``return_direct``, ``metadata`` to familiarize yourself with the parameters of the tool. What is the meaning of the different parameters?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Name: \", wiki_tool.name)\n", + "\n", + "# TODO: insert your code here" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1 (b): Run tool and explore output \n", + "\n", + "**TASK:**\n", + "* Use the ``.run(tool_input)`` method to execute the tool. The ``tool_input`` is the search term that you'd like to query wikipedia with.\n", + "* [Optional] Check out the arguments of the WikipediaAPIWrapper [here](https://api.python.langchain.com/en/latest/utilities/langchain_community.utilities.wikipedia.WikipediaAPIWrapper.html) and modify its parameters above. How does the output change? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tool_input = \"\"\"\n", + "TODO: insert your code here\n", + "\"\"\"\n", + "\n", + "# Run tool\n", + "# TODO: insert your code here" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom tools \n", + "### Custom Wikipedia tool\n", + "\n", + "You can build your own tools and don't have to rely on default tools. Tools can be built from any function with the LangChain class method ``StructuredTool.from_function()``(see [here](https://python.langchain.com/v0.1/docs/modules/tools/custom_tools/#structuredtool-dataclass)). The basic elements are\n", + "* The **function** you would like to be executed when the tool is called\n", + "* The definition of the **input parameters**\n", + "* The tool **description**\n", + "\n", + "The tool description is especially important, since this is what the agent will use to make the deicion if this tool should be used.\n", + "\n", + "Below you see the wikipedia tool, built from the basic elements described above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define the function\n", + "def wikipedia_caller(query:str) ->str:\n", + " \"\"\"This function queries wikipedia through a search query.\"\"\"\n", + " return api_wrapper.run(query)\n", + "\n", + "# Input parameter definition\n", + "class QueryInput(BaseModel):\n", + " query: str = Field(description=\"Input search query\")\n", + "\n", + "# the tool description\n", + "description: str = (\n", + " \"A wrapper around Wikipedia. \"\n", + " \"Useful for when you need to answer general questions about \"\n", + " \"people, places, companies, facts, historical events, or other subjects. \"\n", + " \"Input should be a search query.\"\n", + " )\n", + "\n", + "\n", + "# fuse the function, input parameters and description into a tool. \n", + "my_own_wiki_tool = StructuredTool.from_function(\n", + " func=wikipedia_caller,\n", + " name=\"wikipedia\",\n", + " description=description,\n", + " args_schema=QueryInput,\n", + " return_direct=False,\n", + ")\n", + "\n", + "# test the output of the tool\n", + "print(my_own_wiki_tool.run('pyladies'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2 (a): Build your own Weather tool \n", + "The goal is to build a tool that extracts weather information from the weather site visualcrossing.com. You typically need an API key to extract information from a website. In this example we provide you with the API key. \n", + "\n", + "**TASK:** \n", + "- Build the tool by defining the input parameters and the descriptions. The tool function is already provided to you. \n", + "- Turn function, description and input parameters into a tool through ``StructuredTool.from_function()``.\n", + "- Test if the tool gives an output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define the function\n", + "def extract_city_weather(city:str)->str:\n", + "\n", + " # Build the API URL\n", + " url = f\"https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/{city}?key={WEATHER_KEY}&unitGroup=metric\"\n", + "\n", + " response = requests.get(url)\n", + "\n", + " # extract response\n", + " if response.status_code == 200:\n", + " data = response.json()\n", + " current_temp = data['days'][0]['temp']\n", + " output = f\"Current temperature in {city}: {current_temp}°C\"\n", + " else:\n", + " output = f\"Error: {response.status_code}\"\n", + "\n", + " return output\n", + "\n", + "# Input parameter definition\n", + "class WeatherInput(BaseModel):\n", + " # insert your code here\n", + "\n", + "# the tool description\n", + "description: str = (\n", + " # TODO: insert your code here\n", + " )\n", + "\n", + "# fuse the function, input parameters and description into a tool. \n", + "my_weather_tool = StructuredTool.from_function(\n", + " # TODO: insert your code here\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test the output of your Tool\n", + "print(my_weather_tool.run('Amsterdam'))\n", + "\n", + "# TODO: Try generating more ouputs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's do something even more fun. As we previously saw, we can utilize APIs to build tools. Thinking about APIs, one of the biggest collection of models are available via APIs on HuggingFace. So, how about we try to utilize this. \n", + "\n", + "#### Exercise 2 (b): Build your own Image tool \n", + "The goal is to build a tool that generates an image based on a given prompt. **That means that later when you can build the Agent you can have an LLM that only outputs text, but also images!** \n", + "\n", + "To develop this, you can make use of `mobius`, text-to-image model available on HuggingFace. We provided a HuggingFace token (that you loaded in the start). \n", + "\n", + "**TASK:** \n", + "- Build the tool by defining the input parameters and the descriptions. The tool function is already provided to you. \n", + "- Turn function, description and input parameters into a tool through ``StructuredTool.from_function()``.\n", + "- Test if the tool gives an output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def text_to_image(payload:str):\n", + "\n", + " # Call the text-to-image API with the provided palaod\n", + " API_URL = \"https://api-inference.huggingface.co/models/Corcelio/mobius\"\n", + " headers = {\"Authorization\": f\"Bearer {HUGGING_FACE_KEY}\"}\n", + "\n", + " def query(payload):\n", + " response = requests.post(API_URL, headers=headers, json=payload)\n", + " return response.content\n", + " \n", + " image_bytes = query({\n", + " \"inputs\": payload,\n", + " })\n", + "\n", + " image = Image.open(io.BytesIO(image_bytes))\n", + " \n", + " # Resize the image\n", + " new_size = (400, 400) # Example new size (width, height)\n", + " resized_image = image.resize(new_size)\n", + "\n", + "\n", + " # Save the resized image to a file\n", + " image_path = f'images/image_{payload.replace(\" \", \"_\")}.jpg'\n", + " resized_image.save(image_path)\n", + " \n", + " # Return the path to the saved image\n", + " return f'{image_path} '\n", + "\n", + "\n", + "# Input parameter definition\n", + "class ImageInput(BaseModel):\n", + " payload: str = Field(description=\n", + " # TODO: insert your code here\n", + " )\n", + "\n", + "\n", + "# the tool description\n", + "images_description: str = (\n", + " # TODO: insert your code here\n", + " )\n", + "\n", + "# fuse the function, input parameters and description into a tool. \n", + "my_image_tool = StructuredTool.from_function(\n", + " # TODO: insert your code here\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test the output of your Tool\n", + "print(my_image_tool.run('Amsterdam'))\n", + "\n", + "# TODO: Try generating more ouputs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**If you developed an additional tool, make sure to copy your code in `helper_functions/tools.py` in order to later be able to use your tool in an Agent.**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Combine the individual tools into a list. Your collection of tools is now ready to be used by an agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tools = [my_own_wiki_tool, my_weather_tool, my_image_tool]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "🌟 Done - you are now ready to proceed to the next notebook." + ] + } + ], + "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.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/2_workshop_agents.ipynb b/2_workshop_agents.ipynb new file mode 100644 index 0000000..6471509 --- /dev/null +++ b/2_workshop_agents.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Buildling an LLM Agent with LangChain\n", + "## Notebook 2: Agents\n", + "---\n", + "\n", + "**The goal:** \n", + "\n", + "With this notebook you will familiarize yourself with the key concepts of an LLM agent. At the end, you will have all the code you need for your very own agent that uses the tools that you developed in the previous notebook.\n", + "\n", + "🌟 So ... let us begin! " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Contents:**\n", + "\n", + "1. [Agents](#0)\n", + "2. [Exercise 1: Explore the agent's output](#1)\n", + "3. [Exercise 2 : Build your own agent](#2)\n", + "4. [Exercise 3: Invoke as many tools as you can](#3)\n", + "5. [Exercise 4 : Optimize the agent prompt](#4)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Update helper_functions.keys.py based on private bin link\n", + "from helper_functions.keys import OPENAI_KEY\n", + "\n", + "from helper_functions.tools import *\n", + "from langchain.agents import create_tool_calling_agent # set up the agent\n", + "from langchain.agents import AgentExecutor # execute agent\n", + "from langchain_openai import ChatOpenAI # call openAI as agent llm\n", + "from langchain import hub\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Agents \n", + "\n", + "Agents combine the functionality of two components: LLMs and Tools. They empower an LLM to be able to execute additional tasks and reason through a problem. Namely, LLMs have knowlegde on data that was used at time of training. However, they lack knowledge about up-to-date happenings and information. They consist of the following components: \n", + "- **LLM**: A pre-trained LLM.\n", + "- **List of tools**: List of tools that give additional functionality to the LLM.\n", + "\n", + "One use case of LLM Agents is to make it possible to have LLMs with access to real time information, like the current weather. Namely, when a prompt is called, agents have an LLM and Tools at their disposal. If no tool can be found to help in a answering the question, the agent tries to answer using the raw LLM. E.g. for a given prompt \"What is the **usual** temperature in Amsterdam in summer?\", an LLM will likely already have knowledge. However, a prompt \"What is the **current** temperature in Amsterdam?\", a weather API would be a better source of information, and in this case the Agent will decide to use the information from a weather tool. If such a tool is not available to the agent, the agent will respond that the requested information is not available. \n", + "\n", + "So, basically, you can think of Agents as usual LLMs but with more \"skills\". Cool, right?\n", + "\n", + "Let's see this through an example. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load Tools\n", + "tools = [my_own_wiki_tool, weather_tool, image_tool]\n", + "\n", + "# Load LLM\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0, api_key=OPENAI_KEY)\n", + "\n", + "# With this you let the agent know what its purpose is.\n", + "prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", \"You are a nice assistant\"),\n", + " (\"human\", \"{input}\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\")\n", + "])\n", + "\n", + "# Define the agent (load the LLM and the list of tools)\n", + "agent = create_tool_calling_agent(llm = llm, tools = tools, prompt = prompt)\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n", + "\n", + "print(\"Your agent is ready.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1: Explore the agent's output \n", + "\n", + "**TASK:**\n", + "In the examples below, read the output to observe how the Agent reasons and decides to use a different tool based on the context." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "question_1 = \"Where is Amsterdam?\"\n", + "\n", + "\n", + "print(f\"Question 1: {question_1}\")\n", + "agent_executor.invoke({\"input\": question_1})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "question_2 = \"What is the current temperature in Amsterdam?\"\n", + "\n", + "print(f\"Question 1: {question_2}\")\n", + "agent_executor.invoke({\"input\": question_2})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "question_3 = \"What should I visit in Amsterdam? Show me an photo\"\n", + "\n", + "print(f\"Question 1: {question_3}\")\n", + "agent_executor.invoke({\"input\": question_3})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2 : Build your own agent \n", + "The goal is to build an agent that uses the tools you previously developed. Feel free to also use the pre-made tools, available at `helper_functions/tools.py`\n", + "\n", + "**TASK**: \n", + "- A template for defining an agent and an API key are already provided to you. Build an agent by using a list of tools, and the LLM `gpt-3.5-turbo-0125`.\n", + "- Test if the agent gives an output.\n", + "- Observe how the output changes if you provide less tools in your list of tools.\n", + "- Observe how the output changes if you change the [temperature](https://www.iguazio.com/glossary/llm-temperature/) of the LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Component 1 (Tools): Load Tools\n", + "tools = [\n", + "# TODO: insert your code here\n", + "]\n", + "\n", + "# Component 2 (LLM): Load LLM\n", + "llm = ChatOpenAI(model= # TODO: insert your code here \n", + " temperature=0, \n", + " api_key=OPENAI_KEY)\n", + "\n", + "# Component 3 (Prompt): Let the agent know what its purpose is. For now, let's keep it as is.\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "prompt.messages\n", + "print(type(prompt))\n", + "\n", + "# Define the agent and agent executor (load the LLM, the list of tools, and the prompt (descripiton))\n", + "\n", + "agent = create_tool_calling_agent(\n", + " # TODO: insert your code here\n", + " )\n", + "agent_executor = AgentExecutor(\n", + " # TODO: insert your code here\n", + ")\n", + "\n", + "print(\"Your agent is ready.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 3: Invoke as many tools as you can \n", + "\n", + "**TASK:**\n", + "Try various questions to call the agent and follow the generated reasoning process in the response. The goal is to call the agent in a way thay it will use as many tools as possible. **Let's see who can reach the highest number of tools used with a single prompt!**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "question = \"\"\"\n", + "#TODO: Replace this with your question \n", + "\"\"\"\n", + "\n", + "print(f\"Question: {question}\")\n", + "agent_executor.invoke({\"input\": question})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 4 : Optimize the agent prompt \n", + "So far we played around with the provided LLM and list of tools. Now let's look into the 3rd component: the Agent prompt. \n", + "\n", + "**TASK:**\n", + "- Modify the agent prompt and observe the difference in the output. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Component 1 (Tools): Load Tools from Exercise 1\n", + "tools = tools \n", + "\n", + "# Component 2 (LLM): Load LLM form Exercise 1\n", + "llm = llm\n", + "\n", + "# Component 3 (Prompt): Create your own prompt to instruct the Agent about its purpose.\n", + "your_prompt = \"\"\"\"\n", + " # TODO: enter your code here\n", + "\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", your_prompt),\n", + " (\"human\", \"{input}\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\")\n", + "])\n", + "prompt.messages\n", + "\n", + "\n", + "# This is same as in Exercise 1\n", + "# Define the agent and agent executor (load the LLM, the list of tools, and the prompt (descripiton))\n", + "agent = create_tool_calling_agent(\n", + " llm = llm, tools = tools, prompt = prompt\n", + " )\n", + "agent_executor = AgentExecutor(\n", + " agent=agent, tools=tools, verbose=True\n", + ")\n", + "\n", + "print(\"Your agent is ready.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Observe how the same question from before is answered differently with the different prompt.\n", + "print(f\"Question: {question}\")\n", + "agent_executor.invoke({\"input\": question})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "🌟 Good job! - You are now ready to proceed to the next (optional) notebook." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/3_workshop_advanced.ipynb b/3_workshop_advanced.ipynb new file mode 100644 index 0000000..4dc3502 --- /dev/null +++ b/3_workshop_advanced.ipynb @@ -0,0 +1,278 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# An advanced look at LLM Agents\n", + "## Notebook 3: Agents behind the hood\n", + "---\n", + "\n", + "Previously, we relied on LangChain to to build the agent. However, this is not necessary, since an agent is nothing more than a fancy while loop. \n", + "\n", + "**The goal** \n", + "\n", + "With this notebook you will see what an agent is under the hood, and you can build it based on the LLM output\n", + "\n", + "🌟 So ... let us begin! " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Contents:**\n", + "\n", + "1. [Exercise 1: New tool format](#1)\n", + "2. [Exercise 2: String vs function call response](#2)\n", + "3. [Exercise 3: Understanding the agent while-loop](#3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from helper_functions.helper_functions import simple_description_formatter\n", + "from helper_functions.tools import my_own_wiki_tool, weather_tool\n", + "import pprint\n", + "from helper_functions.keys import client\n", + "import json" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The tools\n", + "\n", + "When querying OpneAI's API directly tools are now called functions (see the API documentation on function calling [here](https://platform.openai.com/docs/assistants/tools/function-calling/quickstart)). Functions have to be passed in a specific JSON format, which we will explore below.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1: New tool format \n", + "\n", + "**TASK:** \n", + "Compile the cell below and then compare the description of the tool `my_own_wiki_tool` to the formatted function description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load the tools and format. In plain OpenAI jargon they are called functions.\n", + "tools = [my_own_wiki_tool, weather_tool]\n", + "function_description = [simple_description_formatter(tool) for tool in tools]\n", + "\n", + "# Store executable functions with their name in dictionary\n", + "available_functions = {tool.name: tool for tool in tools}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Description of the my_own_wiki_tool:\\n{ #TODO: your own code here\n", + "}\\n\")\n", + "\n", + "print(\"Formatted function description of my_own_wiki_tool:\")\n", + "pprint.pprint(function_description[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## The LLM response\n", + "\n", + "The LLM can respond with two types of answers:\n", + "* a **string** that answers the question, \n", + "* a **function-call** object, which contains information on which function to call with which arguments.\n", + "\n", + "The second one can be used to execute function calls." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2: String vs function call response \n", + "\n", + "**TASK:**\n", + "Compile both questions and compare the answers. Do you understand the difference?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "system_prompt = \"You are a friendly, helpful assistant. Your goal is to answer the questions in a concise, but conversational manner.\"\n", + "\n", + "questions = [\"what is the meaning of life?\",\"How many people live in Paris?\"]\n", + "\n", + "for question in questions:\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": question}\n", + " ]\n", + "\n", + " \n", + " response = client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " tools = function_description,\n", + " messages=messages, \n", + " )\n", + "\n", + " print(f\"Question: {question}\")\n", + " print(f\"Answer: {response.choices[0].message.content}\")\n", + " print(f\"Function call: {response.choices[0].message.tool_calls}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## The Agent - a fancy while loop\n", + "\n", + "While the LLM requests function calls we \n", + "* **extract** the **name and arguments** to be called from the initial LLM response,\n", + "* **execute** the **function calls**,\n", + "* **store** the **output of the function** in the messages object,\n", + "* invoke the LLM again, until no function call are requested.\n", + "\n", + "For more details, you can also check out this [OpenAI function calling guide](https://platform.openai.com/docs/guides/function-calling)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 3: Understanding the agent while-loop \n", + "\n", + "**TASK:**\n", + "Complete the code below, then compile the question and investigate the output. Do you understand what you see?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "question = \"which city is bigger: Paris or Munich?\"\n", + "\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": question}\n", + " ]\n", + "\n", + "response = client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " tools = function_description,\n", + " messages=messages, \n", + " )\n", + "\n", + "print('==== Initial LLM response ====')\n", + "print(f\"Answer: {#TODO: your code here\n", + " }\")\n", + "print(f\"Function call: {#TODO: your code here\n", + " }\\n\")\n", + "\n", + "# while the response requests function calls\n", + "while response.choices[0].message.tool_calls:\n", + " \n", + " # store response message with all function calls\n", + " response_message = response.choices[0].message\n", + " messages.append(response_message)\n", + "\n", + " # execute each tool individually\n", + " for tool_call in response.choices[0].message.tool_calls:\n", + " print('==== Function call ====')\n", + "\n", + " # function name and arguments\n", + " function_name = tool_call.function.name\n", + " function_args = json.loads(tool_call.function.arguments)\n", + " print(f'Calling function \"{function_name}\" with arguments {function_args}.')\n", + "\n", + " # execute function call \n", + " function_response = available_functions[function_name].invoke(function_args)\n", + " print(f'Function call response:\\n{function_response}\\n')\n", + "\n", + " # append function response to messages\n", + " messages.append({\n", + " \"tool_call_id\":tool_call.id, \n", + " \"role\": \"tool\", \n", + " \"name\": function_name, \n", + " \"content\": function_response\n", + " })\n", + " \n", + " # get a new response from LLM\n", + " response = client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " tools = function_description,\n", + " messages=messages, \n", + " )\n", + "\n", + " print('==== Intermediate LLM response ====')\n", + " print(f\"Answer: {#TODO: your code here\n", + " \")\n", + " print(f\"Function call: {#TODO: your code here\n", + " }\\n\")\n", + "\n", + "print('==== Final LLM response ====')\n", + "print(\"Question: \", question)\n", + "print(f\"Answer: {response.choices[0].message.content}\")\n", + "print(f\"Function call: {response.choices[0].message.tool_calls}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "🌟 Congratulations - you've finished the workshop" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README.md b/README.md index 99e6cfc..208d765 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,40 @@ - # Introduction to LLM Agents with LangChain -### Presentation: [Presentation_name](workshop/Presentation_template.pptx) + +### Level: Beginner + +### Presentation: [Todo]() ## Workshop description -Describe why your topic is important and what you want to share with your audience + +Welcome to the workshop on building LLM agents with LangChain! + +With this notebook you will familiarize yourself with the key concepts of an LLM agent using LangChain. At the end, you will have all the code you need for your very own agent and you will be able to build custom tools for your own use-case. ## Requirements -Do not forget to indicate Python version and any other tools -+ add requirements.txt or conda.yml or docker image or Binder/Google Collab link -## Usage -* Clone the repository -* Start { TOOL } and navigate to the workshop folder +- Python 3.8 or higher +- Jupyter notebook or jupyter-lab + +## Setting up your environment + +- Clone the repository +- Set up a virtual environment using `virtualenv`: + - `pip install virtualenv` + - Install environment: `python3 -m venv venv` + - Activate enviroment: `source venv/bin/activate` + - Install dependencies in environment: `pip install -r requirements.txt` + - Select `venv` kernel in your notebook + +## API keys + +API keys can abe accessed via this [privatebin](https://privatebin.molops.io/?a6459e88fa282c28#DsFZvkZSiuPcNQzNXvmtvmTozihhaf1hQdqBCd7r3q5s). The password will be shared during the workshop. + +Make sure to paste the API keys into the file `helper_functions/keys.py` before running the notebook. You are ready to go now! ## Video record -Re-watch [this YouTube stream](https://www.youtube.com/watch?v=MjeRY7zNb44&list=PLTdYvc4hjao-4OPJ-thNEYfIcnW6tbPSv) + +Re-watch [Link](https://www.youtube.com/watch?v=MjeRY7zNb44&list=PLTdYvc4hjao-4OPJ-thNEYfIcnW6tbPSv) ## Credits -This workshop was set up by @pyladiesams and {your github handler} + +This workshop was set up by **Ana Chaloska** ([Git](https://github.com/anachaloska), [LinkedIn](https://www.linkedin.com/in/ana-chaloska-809486149/)) and **Maria Bader, PhD,** ([Git](https://github.com/mkmbader), [LinkedIn](https://www.linkedin.com/in/mkmbader/)) and it was powered by [pyladiesams](https://github.com/pyladiesams). \ No newline at end of file diff --git a/helper_functions/helper_functions.py b/helper_functions/helper_functions.py new file mode 100644 index 0000000..27c104a --- /dev/null +++ b/helper_functions/helper_functions.py @@ -0,0 +1,19 @@ +"""Helper functions for the workshop notebooks""" +from langchain.pydantic_v1 import BaseModel + +def simple_description_formatter(tool:BaseModel): + """outputs a json description of the tool.""" + properties = {k:{'type':tool.args[k]['type'], 'description':tool.args[k]['description']} for k in tool.args.keys() } + + return { + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": { + "type": "object", + "properties": properties, + "required": list(tool.args.keys()), + }, + }, + } diff --git a/helper_functions/keys.py b/helper_functions/keys.py new file mode 100644 index 0000000..796f60b --- /dev/null +++ b/helper_functions/keys.py @@ -0,0 +1,9 @@ +# insert here the keys +from openai import OpenAI + +WEATHER_KEY = '' + +OPENAI_KEY = '' +client = OpenAI(api_key = OPENAI_KEY) + +HUGGING_FACE_KEY = '' \ No newline at end of file diff --git a/helper_functions/tools.py b/helper_functions/tools.py new file mode 100644 index 0000000..ec02b48 --- /dev/null +++ b/helper_functions/tools.py @@ -0,0 +1,127 @@ +from langchain.pydantic_v1 import BaseModel, Field +from langchain.tools import StructuredTool +from langchain_community.utilities import WikipediaAPIWrapper +import io +from PIL import Image +import requests +from helper_functions.keys import WEATHER_KEY, HUGGING_FACE_KEY + +WIKI_API_WRAPPER = WikipediaAPIWrapper(top_k_results=1) + +# ------------------------------------------------------------------------------------ +##### Wikipedia tool ##### +def wikipedia_caller(query:str) ->str: + """This function queries wikipedia through a search query.""" + return WIKI_API_WRAPPER.run(query) + +# Input parameter definition +class QueryInput(BaseModel): + query: str = Field(description="Input search query") + +# the tool description +wiki_tool_description: str = ( + "A wrapper around Wikipedia. " + "Useful for when you need to answer general questions about " + "people, places, companies, facts, historical events, or other subjects. " + "Input should be a search query." + ) + +# fuse the function, input parameters and description into a tool. +my_own_wiki_tool = StructuredTool.from_function( + func=wikipedia_caller, + name="wikipedia", + description=wiki_tool_description, + args_schema=QueryInput, + return_direct=False, +) + +# ------------------------------------------------------------------------------------ +##### Weather tool ##### +def extract_city_weather(city:str)->str: + api_key = WEATHER_KEY + + # Build the API URL + url = f"https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/{city}?key={api_key}&unitGroup=metric" + + response = requests.get(url) + + if response.status_code == 200: + data = response.json() + current_temp = data['days'][0]['temp'] + output = f"Current temperature in {city}: {current_temp}°C" + else: + output = f"Error: {response.status_code}" + + return output + +# Input parameter definition +class WeatherInput(BaseModel): + city: str = Field(description="City name") + + +# the tool description +weather_tool_description: str = ( + """ + Allows to extract the current temperature in a specific city. + """ + ) + +# fuse the function, input parameters and description into a tool. +weather_tool = StructuredTool.from_function( + func=extract_city_weather, + name="weather", + description=weather_tool_description, + args_schema=WeatherInput, + return_direct=False, +) +# ------------------------------------------------------------------------------------ +##### Image tool ##### +def text_to_image(payload:str): + API_URL = "https://api-inference.huggingface.co/models/Corcelio/mobius" + headers = {"Authorization": f"Bearer {HUGGING_FACE_KEY}"} + + def query(payload): + response = requests.post(API_URL, headers=headers, json=payload) + return response.content + + image_bytes = query({ + "inputs": payload, + }) + + image = Image.open(io.BytesIO(image_bytes)) + + # Resize the image + new_size = (400, 400) # Example new size (width, height) + resized_image = image.resize(new_size) + + + + # Save the resized image to a file + image_path = f'images/image_{payload.replace(" ", "_")}.jpg' + resized_image.save(image_path) + + # Return the path to the saved image + return f'{image_path} ' + + +# Input parameter definition +class ImageInput(BaseModel): + payload: str = Field(description="What should be converted into image") + + +# the tool description +images_tool_description: str = ( + "Genrate an image based on the input text and return its path" + ) + +# fuse the function, input parameters and description into a tool. +image_tool = StructuredTool.from_function( + func=text_to_image, + name="create_image", + description=images_tool_description, + args_schema=ImageInput, + return_direct=False, +) + +# ------------------------------------------------------------------------------------ +##### TODO: ADD YOUR OWN TOOLS HERE ##### diff --git a/images/image_Amsterdam.jpg b/images/image_Amsterdam.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a73fe54efe3bc91b48826e433616270ec09263a GIT binary patch literal 34975 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d9954b2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +langchain +langchain_openai +langchain_community +wikipedia +langchainhub +pillow \ No newline at end of file diff --git a/solution/images/image_17th-century_canals_of_Amsterdam.jpg b/solution/images/image_17th-century_canals_of_Amsterdam.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4942c4341976bf8cf75f044aba0901435937aa31 GIT binary patch literal 32302 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 C<\n", + "\n", + "**TASK:** \n", + "Compile the cell below and then compare the description of the tool `my_own_wiki_tool` to the formatted function description." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# load the tools and format. In plain OpenAI jargon they are called functions.\n", + "tools = [my_own_wiki_tool, weather_tool]\n", + "function_description = [simple_description_formatter(tool) for tool in tools]\n", + "\n", + "# Store executable functions with their name in dictionary\n", + "available_functions = {tool.name: tool for tool in tools}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Description of the my_own_wiki_tool:\n", + "A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.\n", + "\n", + "Formatted function description of my_own_wiki_tool:\n", + "{'function': {'description': 'A wrapper around Wikipedia. Useful for when you '\n", + " 'need to answer general questions about people, '\n", + " 'places, companies, facts, historical events, or '\n", + " 'other subjects. Input should be a search query.',\n", + " 'name': 'wikipedia',\n", + " 'parameters': {'properties': {'query': {'description': 'Input '\n", + " 'search '\n", + " 'query',\n", + " 'type': 'string'}},\n", + " 'required': ['query'],\n", + " 'type': 'object'}},\n", + " 'type': 'function'}\n" + ] + } + ], + "source": [ + "print(f\"Description of the my_own_wiki_tool:\\n{my_own_wiki_tool.description}\\n\")\n", + "\n", + "print(\"Formatted function description of my_own_wiki_tool:\")\n", + "pprint.pprint(function_description[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## The LLM response\n", + "\n", + "The LLM can respond with two types of answers:\n", + "* a **string** that answers the question, \n", + "* a **function-call** object, which contains information on which function to call with which arguments.\n", + "\n", + "The second one can be used to execute function calls." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2: String vs function call response \n", + "\n", + "**TASK:**\n", + "Compile both questions and compare the answers. Do you understand the difference?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: what is the meaning of life?\n", + "Answer: Ah, the age-old question! The meaning of life is a deep philosophical concept that has fascinated humanity for centuries. While there are many perspectives and beliefs on this topic, some people find meaning through relationships, personal growth, making a positive impact, or pursuing happiness and fulfillment. It's a question that each individual may interpret differently based on their values and experiences.\n", + "Function call: None\n", + "\n", + "Question: How many people live in Paris?\n", + "Answer: None\n", + "Function call: [ChatCompletionMessageToolCall(id='call_kgdkOUCz5gaGbXFwhNgxvcl0', function=Function(arguments='{\"query\":\"Population of Paris\"}', name='wikipedia'), type='function')]\n", + "\n" + ] + } + ], + "source": [ + "system_prompt = \"You are a friendly, helpful assistant. Your goal is to answer the questions in a concise, but conversational manner.\"\n", + "\n", + "questions = [\"what is the meaning of life?\",\"How many people live in Paris?\"]\n", + "\n", + "for question in questions:\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": question}\n", + " ]\n", + "\n", + " \n", + " response = client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " tools = function_description,\n", + " messages=messages, \n", + " )\n", + "\n", + " print(f\"Question: {question}\")\n", + " print(f\"Answer: {response.choices[0].message.content}\")\n", + " print(f\"Function call: {response.choices[0].message.tool_calls}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## The Agent - a fancy while loop\n", + "\n", + "While the LLM requests function calls we \n", + "* **extract** the **name and arguments** to be called from the initial LLM response,\n", + "* **execute** the **function calls**,\n", + "* **store** the **output of the function** in the messages object,\n", + "* invoke the LLM again, until no function call are requested.\n", + "\n", + "For more details, you can also check out this [OpenAI function calling guide](https://platform.openai.com/docs/guides/function-calling)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 3: Understanding the agent while-loop \n", + "\n", + "**TASK:**\n", + "Complete the code below, then compile the question and investigate the output. Do you understand what you see?" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==== Initial LLM response ====\n", + "Answer: None\n", + "Function call: [ChatCompletionMessageToolCall(id='call_yRyw3zVBpLCcNZbF0kB45o9H', function=Function(arguments='{\"query\": \"Paris\"}', name='wikipedia'), type='function'), ChatCompletionMessageToolCall(id='call_LIQltEvoTsHJypyoSyujcIaa', function=Function(arguments='{\"query\": \"Munich\"}', name='wikipedia'), type='function')]\n", + "\n", + "==== Function call ====\n", + "Calling function \"wikipedia\" with arguments {'query': 'Paris'}.\n", + "Function call response:\n", + "Page: Paris\n", + "Summary: Paris is the capital and largest city of France. With an official estimated population of 2,102,650 residents as of 1 January 2023 in an area of more than 105 km2 (41 sq mi), Paris is the fourth-largest city in the European Union and the 30th most densely populated city in the world in 2022. Since the 17th century, Paris has been one of the world's major centres of finance, diplomacy, commerce, culture, fashion, and gastronomy. For its leading role in the arts and sciences, as well as its early and extensive system of street lighting, in the 19th century, it became known as the City of Light. \n", + "The City of Paris is the centre of the Île-de-France region, or Paris Region, with an official estimated population of 12,271,794 inhabitants on 1 January 2023, or about 19% of the population of France. The Paris Region had a GDP of €765 billion (US$1.064 trillion, PPP) in 2021, the highest in the European Union. According to the Economist Intelligence Unit Worldwide Cost of Living Survey, in 2022, Paris was the city with the ninth-highest cost of living in the world.\n", + "Paris is a major railway, highway, and air-transport hub served by two international airports: Charles de Gaulle Airport (the third-busiest airport in Europe) and Orly Airport. Opened in 1900, the city's subway system, the Paris Métro, serves 5.23 million passengers daily; it is the second-busiest metro system in Europe after the Moscow Metro. Gare du Nord is the 24th-busiest railway station in the world and the busiest outside Japan, with 262 million passengers in 2015. Paris has one of the most sustainable transportation systems and is one of only two cities in the world that received the Sustainable Transport Award twice.\n", + "Paris is known for its museums and architectural landmarks: the Louvre received 8.9 million visitors in 2023, on track for keeping its position as the most-visited art museum in the world. The Musée d'Orsay, Musée Marmottan Monet and Musée de l'Orangerie are noted for their collections of French Impressionist art. The Pompidou Centre Musée National d'Art Moderne, Musée Rodin and Musée Picasso are noted for their collections of modern and contemporary art. The historical district along the Seine in the city centre has been classified as a UNESCO World Heritage Site since 1991.\n", + "Paris is home to several United Nations organizations including UNESCO, as well as other international organizations such as the OECD, the OECD Development Centre, the International Bureau of Weights and Measures, the International Energy Agency, the International Federation for Human Rights, along with European bodies such as the European Space Agency, the European Banking Authority and the European Securities and Markets Authority. The football club Paris Saint-Germain and the rugby union club Stade Français are based in Paris. The 81,000-seat Stade de France, built for the 1998 FIFA World Cup, is located just north of Paris in the neighbouring commune of Saint-Denis. Paris hosts the annual French Open Grand Slam tennis tournament on the red clay of Roland Garros. The city hosted the Olympic Games in 1900 and 1924, and will host the 2024 Summer Olympics. The 1938 and 1998 FIFA World Cups, the 2019 FIFA Women's World Cup, the 2007 Rugby World Cup, as well as the 1960, 1984 and 2016 UEFA European Championships were also held in the city. Every July, the Tour de France bicycle race finishes on the Avenue des Champs-Élysées in Paris.\n", + "\n", + "\n", + "\n", + "==== Function call ====\n", + "Calling function \"wikipedia\" with arguments {'query': 'Munich'}.\n", + "Function call response:\n", + "Page: Munich\n", + "Summary: Munich ( MEW-nik(h); German: München [ˈmʏnçn̩] ) is the capital and most populous city of the Free State of Bavaria, Germany. With a population of 1,589,706 inhabitants as of 29 February 2024, it is the third-largest city in Germany, after Berlin and Hamburg, and thus the largest which does not constitute its own state, as well as the 11th-largest city in the European Union. The city's metropolitan region is home to about 6.2 million people and the third largest metropolitan region by GDP in the European Union.\n", + "Straddling the banks of the river Isar north of the Alps, Munich is the seat of the Bavarian administrative region of Upper Bavaria, while being the most densely populated municipality in Germany with 4,500 people per km2. Munich is the second-largest city in the Bavarian dialect area, after the Austrian capital of Vienna.\n", + "The city was first mentioned in 1158. Catholic Munich strongly resisted the Reformation and was a political point of divergence during the resulting Thirty Years' War, but remained physically untouched despite an occupation by the Protestant Swedes. Once Bavaria was established as the Kingdom of Bavaria in 1806, Munich became a major European centre of arts, architecture, culture and science. In 1918, during the German Revolution of 1918–19, the ruling House of Wittelsbach, which had governed Bavaria since 1180, was forced to abdicate in Munich and a short-lived Bavarian Soviet Republic was declared. In the 1920s, Munich became home to several political factions, among them the Nazi Party. After the Nazis' rise to power, Munich was declared their \"Capital of the Movement\". The city was heavily bombed during World War II, but has restored most of its old town and boasts nearly 30.000 buildings from before the war all over the city. After the end of postwar American occupation in 1949, there was a great increase in population and economic power during the years of Wirtschaftswunder. The city hosted the 1972 Summer Olympics.\n", + "Today, Munich is a global centre of science, technology, finance, innovation, business, and tourism. Munich enjoys a very high standard and quality of living, reaching first in Germany and third worldwide according to the 2018 Mercer survey, and being rated the world's most liveable city by the Monocle's Quality of Life Survey 2018. Munich is consistently ranked as one of the most expensive cities in Germany in terms of real estate prices and rental costs.\n", + "In 2021, 28.8 percent of Munich's residents were foreigners, and another 17.7 percent were German citizens with a migration background from a foreign country. Munich's economy is based on high tech, automobiles, and the service sector, as well as IT, biotechnology, engineering, and electronics. It has one of the strongest economies of any German city and the lowest unemployment rate of all cities in Germany with more than one million inhabitants. The city houses many multinational companies, such as BMW, Siemens, Allianz SE and Munich Re. In addition, Munich is home to two research universities, and a multitude of scientific institutions. Munich's numerous architectural and cultural attractions, sports events, exhibitions and its annual Oktoberfest, the world's largest Volksfest, attract considerable tourism.\n", + "\n", + "== Intermediate LLM response ==\n", + "Answer: Paris is the capital and largest city of France with an official estimated population of 2,102,650 residents. Munich, on the other hand, is the third-largest city in Germany with a population of 1,589,706 inhabitants. Therefore, Paris is bigger in terms of population compared to Munich. \n", + "Function call: None\n", + "\n", + "==== Final LLM response ====\n", + "Question: which city is bigger: Paris or Munich?\n", + "Answer: Paris is the capital and largest city of France with an official estimated population of 2,102,650 residents. Munich, on the other hand, is the third-largest city in Germany with a population of 1,589,706 inhabitants. Therefore, Paris is bigger in terms of population compared to Munich.\n", + "Function call: None\n", + "\n" + ] + } + ], + "source": [ + "question = \"which city is bigger: Paris or Munich?\"\n", + "\n", + "messages = [\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": question}\n", + " ]\n", + "\n", + "response = client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " tools = function_description,\n", + " messages=messages, \n", + " )\n", + "\n", + "print('==== Initial LLM response ====')\n", + "print(f\"Answer: {response.choices[0].message.content}\")\n", + "print(f\"Function call: {response.choices[0].message.tool_calls}\\n\")\n", + "\n", + "# while the response requests function calls\n", + "while response.choices[0].message.tool_calls:\n", + " \n", + " # store response message with all function calls\n", + " response_message = response.choices[0].message\n", + " messages.append(response_message)\n", + "\n", + " # execute each tool individually\n", + " for tool_call in response.choices[0].message.tool_calls:\n", + " print('==== Function call ====')\n", + "\n", + " # function name and arguments\n", + " function_name = tool_call.function.name\n", + " function_args = json.loads(tool_call.function.arguments)\n", + " print(f'Calling function \"{function_name}\" with arguments {function_args}.')\n", + "\n", + " # execute function call \n", + " function_response = available_functions[function_name].invoke(function_args)\n", + " print(f'Function call response:\\n{function_response}\\n')\n", + "\n", + " # append function response to messages\n", + " messages.append({\n", + " \"tool_call_id\":tool_call.id, \n", + " \"role\": \"tool\", \n", + " \"name\": function_name, \n", + " \"content\": function_response\n", + " })\n", + " \n", + " # get a new response from LLM\n", + " response = client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " tools = function_description,\n", + " messages=messages, \n", + " )\n", + "\n", + " print('==== Intermediate LLM response ====')\n", + " print(f\"Answer: {response.choices[0].message.content} \")\n", + " print(f\"Function call: {response.choices[0].message.tool_calls }\\n\")\n", + "\n", + "print('==== Final LLM response ====')\n", + "print(\"Question: \", question)\n", + "print(f\"Answer: {response.choices[0].message.content}\")\n", + "print(f\"Function call: {response.choices[0].message.tool_calls}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "🌟 Congratulations - you've finished the workshop" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/solution/solution_agents.ipynb b/solution/solution_agents.ipynb new file mode 100644 index 0000000..9b3ba3a --- /dev/null +++ b/solution/solution_agents.ipynb @@ -0,0 +1,543 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Buildling an LLM Agent with LangChain \n", + "## Solutions: Agents\n", + "\n", + "\n", + "**Content:**\n", + "\n", + "1. [Exercise 1: Explore the agent's output](#1)\n", + "2. [Exercise 2 : Build your own agent](#2)\n", + "3. [Exercise 3: Invoke as many tools as you can](#3)\n", + "4. [Exercise 4 : Optimize the agent prompt](#4)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "current_dir = os.path.dirname(os.path.abspath('.'))\n", + "folder_b_path = os.path.join(current_dir, 'helper_functions')\n", + "sys.path.append(current_dir)\n", + "\n", + "from helper_functions.keys import OPENAI_KEY\n", + "from helper_functions.tools import my_own_wiki_tool, weather_tool, image_tool\n", + "\n", + "from langchain.agents import create_tool_calling_agent # set up the agent\n", + "from langchain.agents import AgentExecutor # execute agent\n", + "from langchain_openai import ChatOpenAI # call openAI as agent llm\n", + "from langchain import hub\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1: Explore the agent's output \n", + "\n", + "**TASK:**\n", + "In the examples below, read the output to observe how the Agent reasons and decides to use a different tool based on the context." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Your agent is ready.\n" + ] + } + ], + "source": [ + "# Agent\n", + "\n", + "# Load Tools\n", + "tools = [my_own_wiki_tool, weather_tool, image_tool]\n", + "\n", + "# Load LLM\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0, api_key=OPENAI_KEY)\n", + "\n", + "# Get the prompt to use - you can modify this! With this you let the agent know what its purpose is.\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "prompt.messages\n", + "print(type(prompt))\n", + "\n", + "# Define the agent (load the LLM and the list of tools)\n", + "agent = create_tool_calling_agent(llm = llm, tools = tools, prompt = prompt)\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n", + "\n", + "print(\"Your agent is ready.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question 1: Where is Amsterdam?\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `wikipedia` with `{'query': 'Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mPage: Amsterdam\n", + "Summary: Amsterdam ( AM-stər-dam, UK also AM-stər-DAM, Dutch: [ˌɑmstərˈdɑm] ; literally, \"The Dam on the River Amstel\") is the capital and most populated city of the Netherlands. It has a population of 921,402 within the city proper, 1,457,018 in the urban area and 2,480,394 in the metropolitan area. Located in the Dutch province of North Holland, Amsterdam is colloquially referred to as the \"Venice of the North\", for its large number of canals, now a UNESCO World Heritage Site.\n", + "Amsterdam was founded at the mouth of the Amstel River, which was dammed to control flooding. Originally a small fishing village in the 12th century, Amsterdam became a major world port during the Dutch Golden Age of the 17th century, when the Netherlands was an economic powerhouse. Amsterdam was the leading centre for finance and trade, as well as a hub of secular art production. In the 19th and 20th centuries, the city expanded and new neighborhoods and suburbs were built. The city has a long tradition of openness, liberalism, and tolerance. Cycling is key to the city's modern character, and there are numerous biking paths and lanes spread throughout.\n", + "Amsterdam's main attractions include its historic canals; the Rijksmuseum, the state museum with Dutch Golden Age art; the Van Gogh Museum; the Dam Square, where the Royal Palace of Amsterdam and former city hall are located; the Amsterdam Museum; Stedelijk Museum, with modern art; the Concertgebouw concert hall; the Anne Frank House; the Scheepvaartmuseum, the Natura Artis Magistra; Hortus Botanicus, NEMO, the red-light district and cannabis coffee shops. The city is known for its nightlife and festival activity, with several nightclubs among the world's most famous. Its artistic heritage, canals and narrow canal houses with gabled façades, well-preserved legacies of the city's 17th-century Golden Age, have attracted millions of visitors annually.\n", + "The Amsterdam Stock Exchange, founded in 1602, is considered the oldest \"modern\" securities market stock exchange in the world. As the commercial capital of the Netherlands and one of the top financial centres in Europe, Amsterdam is considered an alpha world city. The city is the cultural capital of the Netherlands. Many large Dutch institutions have their headquarters in the city. Many of the world's largest companies are based here or have established their European headquarters in the city, such as technology companies Uber, Netflix and Tesla. In 2022, Amsterdam was ranked the ninth-best city to live in by the Economist Intelligence Unit and 12th on quality of living for environment and infrastructure by Mercer. The city was ranked 4th place globally as top tech hub in 2019. The Port of Amsterdam is the fifth largest in Europe. The KLM hub and Amsterdam's main airport, Schiphol, is the busiest airport in the Netherlands, third in Europe, and 11th in the world. The Dutch capital is one of the most multicultural cities in the world, with about 180 nationalities represented. Immigration and ethnic segregation in Amsterdam is a current issue.\n", + "Amsterdam's notable residents throughout its history include painters Rembrandt and Vincent van Gogh, 17th-century philosophers Baruch Spinoza, John Locke, René Descartes, and the Holocaust victim and diarist Anne Frank.\u001b[0m\u001b[32;1m\u001b[1;3mAmsterdam is the capital and most populated city of the Netherlands. It is located in the Dutch province of North Holland. Amsterdam is colloquially referred to as the \"Venice of the North\" due to its large number of canals, which are now a UNESCO World Heritage Site.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Where is Amsterdam?',\n", + " 'output': 'Amsterdam is the capital and most populated city of the Netherlands. It is located in the Dutch province of North Holland. Amsterdam is colloquially referred to as the \"Venice of the North\" due to its large number of canals, which are now a UNESCO World Heritage Site.'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question_1 = \"Where is Amsterdam?\"\n", + "\n", + "\n", + "print(f\"Question 1: {question_1}\")\n", + "agent_executor.invoke({\"input\": question_1})" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question 1: What is the current temperature in Amsterdam?\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `weather` with `{'city': 'Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3mCurrent temperature in Amsterdam: 16.1°C\u001b[0m\u001b[32;1m\u001b[1;3mThe current temperature in Amsterdam is 16.1°C.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'What is the current temperature in Amsterdam?',\n", + " 'output': 'The current temperature in Amsterdam is 16.1°C.'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question_2 = \"What is the current temperature in Amsterdam?\"\n", + "\n", + "print(f\"Question 1: {question_2}\")\n", + "agent_executor.invoke({\"input\": question_2})" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question 1: What should I visit in Amsterdam? Show me an photo\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `wikipedia` with `{'query': 'Tourist attractions in Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mPage: List of tourist attractions in Amsterdam\n", + "Summary: Amsterdam, one of Europe's capitals, has many attractions for visitors. The city's most famous sight is the 17th-century canals of Amsterdam (in Dutch: grachtengordel), located in the heart of Amsterdam, have been added to the UNESCO World Heritage List.\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `create_image` with `{'payload': '17th-century canals of Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3mimages/image_17th-century_canals_of_Amsterdam.jpg \u001b[0m\u001b[32;1m\u001b[1;3mAmsterdam is known for its 17th-century canals, which are a UNESCO World Heritage site. Here is a photo of the canals for you to see: \n", + "\n", + "![17th-century canals of Amsterdam](images/image_17th-century_canals_of_Amsterdam.jpg)\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'What should I visit in Amsterdam? Show me an photo',\n", + " 'output': 'Amsterdam is known for its 17th-century canals, which are a UNESCO World Heritage site. Here is a photo of the canals for you to see: \\n\\n![17th-century canals of Amsterdam](images/image_17th-century_canals_of_Amsterdam.jpg)'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question_3 = \"What should I visit in Amsterdam? Show me an photo\"\n", + "\n", + "print(f\"Question 1: {question_3}\")\n", + "agent_executor.invoke({\"input\": question_3})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2 : Build your own agent \n", + "The goal is to build an agent that uses the tools you previously developed. Feel free to also use the pre-made tools, available at `helper_functions/tools.py`\n", + "\n", + "**TASK**: \n", + "- A template for defining an agent and an API key are already provided to you. Build an agent by using a list of tools, and the LLM `gpt-3.5-turbo-0125`. A template for this agent follows below.\n", + "- Test if the agent gives an output.\n", + "- Observe how the output changes if you provide less tools in your list of tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Your agent is ready.\n" + ] + } + ], + "source": [ + "# Component 1 (Tools): Load Tools\n", + "tools = [\n", + " my_own_wiki_tool, weather_tool, image_tool\n", + "]\n", + "\n", + "# Component 2 (LLM): Load LLM\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", \n", + " temperature=0, \n", + " api_key=OPENAI_KEY)\n", + "\n", + "# Component 3 (Prompt): Let the agent know what its purpose is. For now, let's keep it as is.\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "prompt.messages\n", + "print(type(prompt))\n", + "\n", + "# Define the agent and agent executor (load the LLM, the list of tools, and the prompt (descripiton))\n", + "\n", + "agent = create_tool_calling_agent(\n", + " llm = llm, tools = tools, prompt = prompt\n", + " )\n", + "agent_executor = AgentExecutor(\n", + " agent=agent, tools=tools, verbose=True\n", + ")\n", + "\n", + "print(\"Your agent is ready.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 3: Invoke as many tools as you can \n", + "\n", + "**TASK:**\n", + "Try various questions to call the agent and follow the generated reasoning process in the response. The goal is to call the agent in a way thay it will use as many tools as possible. **Let's see who can reach the highest number of tools used with a single prompt!**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:** (invoking 2 tools: Wiki and Weather. Can you reach more?)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: \n", + "Where is Amsterdam and what is the weather like?\n", + "\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `wikipedia` with `{'query': 'Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mPage: Amsterdam\n", + "Summary: Amsterdam ( AM-stər-dam, UK also AM-stər-DAM, Dutch: [ˌɑmstərˈdɑm] ; literally, \"The Dam on the River Amstel\") is the capital and most populated city of the Netherlands. It has a population of 921,402 within the city proper, 1,457,018 in the urban area and 2,480,394 in the metropolitan area. Located in the Dutch province of North Holland, Amsterdam is colloquially referred to as the \"Venice of the North\", for its large number of canals, now a UNESCO World Heritage Site.\n", + "Amsterdam was founded at the mouth of the Amstel River, which was dammed to control flooding. Originally a small fishing village in the 12th century, Amsterdam became a major world port during the Dutch Golden Age of the 17th century, when the Netherlands was an economic powerhouse. Amsterdam was the leading centre for finance and trade, as well as a hub of secular art production. In the 19th and 20th centuries, the city expanded and new neighborhoods and suburbs were built. The city has a long tradition of openness, liberalism, and tolerance. Cycling is key to the city's modern character, and there are numerous biking paths and lanes spread throughout.\n", + "Amsterdam's main attractions include its historic canals; the Rijksmuseum, the state museum with Dutch Golden Age art; the Van Gogh Museum; the Dam Square, where the Royal Palace of Amsterdam and former city hall are located; the Amsterdam Museum; Stedelijk Museum, with modern art; the Concertgebouw concert hall; the Anne Frank House; the Scheepvaartmuseum, the Natura Artis Magistra; Hortus Botanicus, NEMO, the red-light district and cannabis coffee shops. The city is known for its nightlife and festival activity, with several nightclubs among the world's most famous. Its artistic heritage, canals and narrow canal houses with gabled façades, well-preserved legacies of the city's 17th-century Golden Age, have attracted millions of visitors annually.\n", + "The Amsterdam Stock Exchange, founded in 1602, is considered the oldest \"modern\" securities market stock exchange in the world. As the commercial capital of the Netherlands and one of the top financial centres in Europe, Amsterdam is considered an alpha world city. The city is the cultural capital of the Netherlands. Many large Dutch institutions have their headquarters in the city. Many of the world's largest companies are based here or have established their European headquarters in the city, such as technology companies Uber, Netflix and Tesla. In 2022, Amsterdam was ranked the ninth-best city to live in by the Economist Intelligence Unit and 12th on quality of living for environment and infrastructure by Mercer. The city was ranked 4th place globally as top tech hub in 2019. The Port of Amsterdam is the fifth largest in Europe. The KLM hub and Amsterdam's main airport, Schiphol, is the busiest airport in the Netherlands, third in Europe, and 11th in the world. The Dutch capital is one of the most multicultural cities in the world, with about 180 nationalities represented. Immigration and ethnic segregation in Amsterdam is a current issue.\n", + "Amsterdam's notable residents throughout its history include painters Rembrandt and Vincent van Gogh, 17th-century philosophers Baruch Spinoza, John Locke, René Descartes, and the Holocaust victim and diarist Anne Frank.\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `weather` with `{'city': 'Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3mCurrent temperature in Amsterdam: 16.1°C\u001b[0m\u001b[32;1m\u001b[1;3mAmsterdam is the capital and most populated city of the Netherlands. It is located in the Dutch province of North Holland and is colloquially referred to as the \"Venice of the North\" due to its large number of canals, which are now a UNESCO World Heritage Site. Amsterdam has a population of 921,402 within the city proper, 1,457,018 in the urban area, and 2,480,394 in the metropolitan area.\n", + "\n", + "Some of the main attractions in Amsterdam include historic canals, the Rijksmuseum, the Van Gogh Museum, Dam Square, the Royal Palace of Amsterdam, the Anne Frank House, and the red-light district. The city is known for its nightlife, festival activity, and artistic heritage.\n", + "\n", + "The current temperature in Amsterdam is 16.1°C.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': '\\nWhere is Amsterdam and what is the weather like?\\n',\n", + " 'output': 'Amsterdam is the capital and most populated city of the Netherlands. It is located in the Dutch province of North Holland and is colloquially referred to as the \"Venice of the North\" due to its large number of canals, which are now a UNESCO World Heritage Site. Amsterdam has a population of 921,402 within the city proper, 1,457,018 in the urban area, and 2,480,394 in the metropolitan area.\\n\\nSome of the main attractions in Amsterdam include historic canals, the Rijksmuseum, the Van Gogh Museum, Dam Square, the Royal Palace of Amsterdam, the Anne Frank House, and the red-light district. The city is known for its nightlife, festival activity, and artistic heritage.\\n\\nThe current temperature in Amsterdam is 16.1°C.'}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question = \"\"\"\n", + "Where is Amsterdam and what is the weather like?\n", + "\"\"\"\n", + "\n", + "print(f\"Question: {question}\")\n", + "agent_executor.invoke({\"input\": question})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 4 : Optimize the agent prompt \n", + "So far we played around with the provided LLM and list of tools. Now let's look into the 3rd component: the Agent prompt. \n", + "\n", + "**TASK:**\n", + "- Modify the agent prompt and observe the difference in the output. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**\n", + "\n", + "In this solution we assume you are building an agent for a travel agency, and you want the agent to help you motivate people to visit a city." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Your agent is ready.\n" + ] + } + ], + "source": [ + "# Component 1 (Tools): Load Tools from Exercise 1\n", + "tools = tools \n", + "\n", + "# Component 2 (LLM): Load LLM form Exercise 1\n", + "llm = llm\n", + "\n", + "your_prompt = \"\"\"\"\n", + " You are a helpful assisstant, trying to motivate people to go on holiday.\n", + "\"\"\"\n", + "\n", + "# Component 3 (Prompt): Create your own prompt to instruct the Agent about its purpose.\n", + "prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", your_prompt),\n", + " (\"human\", \"{input}\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\")\n", + "])\n", + "prompt.messages\n", + "\n", + "\n", + "# Define the agent and agent executor (load the LLM, the list of tools, and the prompt (descripiton))\n", + "# This is same as in Exercise 1\n", + "agent = create_tool_calling_agent(\n", + " llm = llm, tools = tools, prompt = prompt\n", + " )\n", + "agent_executor = AgentExecutor(\n", + " agent=agent, tools=tools, verbose=True\n", + ")\n", + "\n", + "print(\"Your agent is ready.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: \n", + "Where is Amsterdam and what is the weather like?\n", + "\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `wikipedia` with `{'query': 'Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mPage: Amsterdam\n", + "Summary: Amsterdam ( AM-stər-dam, UK also AM-stər-DAM, Dutch: [ˌɑmstərˈdɑm] ; literally, \"The Dam on the River Amstel\") is the capital and most populated city of the Netherlands. It has a population of 921,402 within the city proper, 1,457,018 in the urban area and 2,480,394 in the metropolitan area. Located in the Dutch province of North Holland, Amsterdam is colloquially referred to as the \"Venice of the North\", for its large number of canals, now a UNESCO World Heritage Site.\n", + "Amsterdam was founded at the mouth of the Amstel River, which was dammed to control flooding. Originally a small fishing village in the 12th century, Amsterdam became a major world port during the Dutch Golden Age of the 17th century, when the Netherlands was an economic powerhouse. Amsterdam was the leading centre for finance and trade, as well as a hub of secular art production. In the 19th and 20th centuries, the city expanded and new neighborhoods and suburbs were built. The city has a long tradition of openness, liberalism, and tolerance. Cycling is key to the city's modern character, and there are numerous biking paths and lanes spread throughout.\n", + "Amsterdam's main attractions include its historic canals; the Rijksmuseum, the state museum with Dutch Golden Age art; the Van Gogh Museum; the Dam Square, where the Royal Palace of Amsterdam and former city hall are located; the Amsterdam Museum; Stedelijk Museum, with modern art; the Concertgebouw concert hall; the Anne Frank House; the Scheepvaartmuseum, the Natura Artis Magistra; Hortus Botanicus, NEMO, the red-light district and cannabis coffee shops. The city is known for its nightlife and festival activity, with several nightclubs among the world's most famous. Its artistic heritage, canals and narrow canal houses with gabled façades, well-preserved legacies of the city's 17th-century Golden Age, have attracted millions of visitors annually.\n", + "The Amsterdam Stock Exchange, founded in 1602, is considered the oldest \"modern\" securities market stock exchange in the world. As the commercial capital of the Netherlands and one of the top financial centres in Europe, Amsterdam is considered an alpha world city. The city is the cultural capital of the Netherlands. Many large Dutch institutions have their headquarters in the city. Many of the world's largest companies are based here or have established their European headquarters in the city, such as technology companies Uber, Netflix and Tesla. In 2022, Amsterdam was ranked the ninth-best city to live in by the Economist Intelligence Unit and 12th on quality of living for environment and infrastructure by Mercer. The city was ranked 4th place globally as top tech hub in 2019. The Port of Amsterdam is the fifth largest in Europe. The KLM hub and Amsterdam's main airport, Schiphol, is the busiest airport in the Netherlands, third in Europe, and 11th in the world. The Dutch capital is one of the most multicultural cities in the world, with about 180 nationalities represented. Immigration and ethnic segregation in Amsterdam is a current issue.\n", + "Amsterdam's notable residents throughout its history include painters Rembrandt and Vincent van Gogh, 17th-century philosophers Baruch Spinoza, John Locke, René Descartes, and the Holocaust victim and diarist Anne Frank.\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `weather` with `{'city': 'Amsterdam'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3mCurrent temperature in Amsterdam: 16.1°C\u001b[0m\u001b[32;1m\u001b[1;3mAmsterdam is the capital and most populated city of the Netherlands. It is located in the Dutch province of North Holland and is colloquially referred to as the \"Venice of the North\" due to its large number of canals, which are now a UNESCO World Heritage Site. Amsterdam has a rich history dating back to the 12th century and was a major world port during the Dutch Golden Age of the 17th century.\n", + "\n", + "Some of the main attractions in Amsterdam include historic canals, the Rijksmuseum, the Van Gogh Museum, Dam Square, the Anne Frank House, and the red-light district. The city is known for its nightlife, festivals, and cultural heritage.\n", + "\n", + "The current temperature in Amsterdam is 16.1°C. It's a great time to visit and explore this vibrant city!\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': '\\nWhere is Amsterdam and what is the weather like?\\n',\n", + " 'output': 'Amsterdam is the capital and most populated city of the Netherlands. It is located in the Dutch province of North Holland and is colloquially referred to as the \"Venice of the North\" due to its large number of canals, which are now a UNESCO World Heritage Site. Amsterdam has a rich history dating back to the 12th century and was a major world port during the Dutch Golden Age of the 17th century.\\n\\nSome of the main attractions in Amsterdam include historic canals, the Rijksmuseum, the Van Gogh Museum, Dam Square, the Anne Frank House, and the red-light district. The city is known for its nightlife, festivals, and cultural heritage.\\n\\nThe current temperature in Amsterdam is 16.1°C. It\\'s a great time to visit and explore this vibrant city!'}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Observe how the same question from before is answered differently with the different prompt.\n", + "print(f\"Question: {question}\")\n", + "agent_executor.invoke({\"input\": question})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/solution/solution_tools.ipynb b/solution/solution_tools.ipynb new file mode 100644 index 0000000..0d4423e --- /dev/null +++ b/solution/solution_tools.ipynb @@ -0,0 +1,353 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction to LLM Agents with LangChain\n", + "## Solutions: Tools\n", + "\n", + "**Content:**\n", + "1. [Exercise 1 (a): Explore tool parameters](#1a)\n", + "2. [Exercise 1 (b): Run tool and explore output](#1b)\n", + "3. [Exercise 2 (a): Build your own Weather tool](#2a)\n", + "4. [Exercise 2 (b): Build your own Image tool](#2b)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "current_dir = os.path.dirname(os.path.abspath('.'))\n", + "folder_b_path = os.path.join(current_dir, 'helper_functions')\n", + "sys.path.append(current_dir)\n", + "\n", + "import requests\n", + "from helper_functions.keys import WEATHER_KEY, HUGGING_FACE_KEY\n", + "from langchain_community.tools import WikipediaQueryRun\n", + "from langchain_community.utilities import WikipediaAPIWrapper\n", + "from langchain.pydantic_v1 import BaseModel, Field\n", + "from langchain.tools import StructuredTool\n", + "from PIL import Image\n", + "import io" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1 (a): Explore tool parameters \n", + "\n", + "**Task:** \n", + "Use the methods ``name``, ``description``, ``args``, ``return_direct``, ``metadata`` to familiarize yourself with the parameters of the tool. What is the meaning of the different parameters?\n", + "\n", + "**Background:** \n", + "Each tool is a ``BaseTool`` class object, you can find its definition [here](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.BaseTool.html#langchain_core.tools.BaseTool)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "api_wrapper = WikipediaAPIWrapper(top_k_results=1)\n", + "wiki_tool = WikipediaQueryRun(api_wrapper=api_wrapper)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Name: wikipedia\n", + "Description: A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.\n", + "Input argument schema: {'query': {'title': 'Query', 'description': 'query to look up on wikipedia', 'type': 'string'}}\n", + "Return output to user? False\n", + "Metadata: None\n" + ] + } + ], + "source": [ + "print(\"Name: \", wiki_tool.name)\n", + "print(\"Description: \", wiki_tool.description)\n", + "print(\"Input argument schema: \", wiki_tool.args)\n", + "print(\"Return output to user? \", wiki_tool.return_direct)\n", + "print(\"Metadata: \", wiki_tool.metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 1 (b): Run tool and explore output \n", + "\n", + "**TASK:**\n", + "* Use the ``.run(tool_input)`` method to execute the tool. The ``tool_input`` is the search term that you'd like to query wikipedia with.\n", + "* [Optional] Check out the arguments of the WikipediaAPIWrapper [here](https://api.python.langchain.com/en/latest/utilities/langchain_community.utilities.wikipedia.WikipediaAPIWrapper.html) and modify its parameters above. How does the output change? " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Page: PyLadies\n", + "Summary: PyLadies is an international mentorship group which focuses on helping more women become active participants in the Python open-source community. It is part of the Python Software Foundation. It was started in Los Angeles in 2011. The mission of the group is to create a diverse Python community through outreach, education, conferences and social gatherings. PyLadies also provides funding for women to attend open source conferences. The aim of PyLadies is increasing the participation of women in computing. PyLadies became a multi-chapter organization with the founding of the Washington, D.C., chapter in August 2011.\n" + ] + } + ], + "source": [ + "tool_input = 'pyladies'\n", + "\n", + "print(wiki_tool.run(tool_input))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2 (a): Build your own Weather tool \n", + "The goal is to build a tool that extracts weather information from the weather site visualcrossing.com. You typically need an API key to extract information from a website. In this example we provide you with the API key. \n", + "\n", + "**TASK:** \n", + "- Build the tool by defining the input parameters and the descriptions. The tool function is already provided to you. \n", + "- Turn function, description and input parameters into a tool through ``StructuredTool.from_function()``.\n", + "- Test if the tool gives an output." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current temperature in Amsterdam: 16.1°C\n" + ] + } + ], + "source": [ + "# Define the function\n", + "def extract_city_weather(city:str)->str:\n", + "\n", + " # Build the API URL\n", + " url = f\"https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/{city}?key={WEATHER_KEY}&unitGroup=metric\"\n", + "\n", + " response = requests.get(url)\n", + "\n", + " # extract response\n", + " if response.status_code == 200:\n", + " data = response.json()\n", + " current_temp = data['days'][0]['temp']\n", + " output = f\"Current temperature in {city}: {current_temp}°C\"\n", + " else:\n", + " output = f\"Error: {response.status_code}\"\n", + "\n", + " return output\n", + "\n", + "# Input parameter definition\n", + "class WeatherInput(BaseModel):\n", + " city: str = Field(description=\"City name\")\n", + "\n", + "\n", + "# the tool description\n", + "description: str = (\n", + " \"Allows to extract the current temperature in a specific city\"\n", + " )\n", + "\n", + "# fuse the function, input parameters and description into a tool. \n", + "weather_tool = StructuredTool.from_function(\n", + " func=extract_city_weather,\n", + " name=\"weather\",\n", + " description=description,\n", + " args_schema=WeatherInput,\n", + " return_direct=False,\n", + ")\n", + "\n", + "# test the output of the tool\n", + "print(weather_tool.run('Amsterdam'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Exercise 2 (b): Build your own Image tool \n", + "The goal is to build a tool that generates an image based on a given prompt. **That means that later when you can build the Agent you can have an LLM that only outputs text, but also images!** \n", + "\n", + "To develop this, you can make use of `mobius`, text-to-image model available on HuggingFace. We provided a HuggingFace token (that you loaded in the start). \n", + "\n", + "**TASK:** \n", + "- Build the tool by defining the input parameters and the descriptions. The tool function is already provided to you. \n", + "- Turn function, description and input parameters into a tool through ``StructuredTool.from_function()``.\n", + "- Test if the tool gives an output." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**SOLUTION:**" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'images/image_cat_in_a_white_box.jpg '" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def text_to_image(payload:str):\n", + "\n", + " # Call the text-to-image API with the provided palaod\n", + " API_URL = \"https://api-inference.huggingface.co/models/Corcelio/mobius\"\n", + " headers = {\"Authorization\": f\"Bearer {HUGGING_FACE_KEY}\"}\n", + "\n", + " def query(payload):\n", + " response = requests.post(API_URL, headers=headers, json=payload)\n", + " return response.content\n", + " \n", + " image_bytes = query({\n", + " \"inputs\": payload,\n", + " })\n", + "\n", + " image = Image.open(io.BytesIO(image_bytes))\n", + " \n", + " # Resize the image\n", + " new_size = (400, 400) # Example new size (width, height)\n", + " resized_image = image.resize(new_size)\n", + "\n", + "\n", + " # Save the resized image to a file\n", + " image_path = f'images/image_{payload.replace(\" \", \"_\")}.jpg'\n", + " resized_image.save(image_path)\n", + " \n", + " # Return the path to the saved image\n", + " return f'{image_path} '\n", + "\n", + "\n", + "# Input parameter definition\n", + "class ImageInput(BaseModel):\n", + " payload: str = Field(description=\"What should be converted into image\")\n", + "\n", + "\n", + "# the tool description\n", + "images_tool_description: str = (\n", + " \"Genrate an image based on the input text and return its path\"\n", + " )\n", + "\n", + "# fuse the function, input parameters and description into a tool. \n", + "image_tool = StructuredTool.from_function(\n", + " func=text_to_image,\n", + " name=\"create_image\",\n", + " description=images_tool_description,\n", + " args_schema=ImageInput,\n", + " return_direct=False,\n", + ")\n", + "image_tool.run('cat in a white box')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/solutions/Solutions_to_your_workshop.ipynb b/solutions/Solutions_to_your_workshop.ipynb deleted file mode 100644 index 2fd6442..0000000 --- a/solutions/Solutions_to_your_workshop.ipynb +++ /dev/null @@ -1,6 +0,0 @@ -{ - "cells": [], - "metadata": {}, - "nbformat": 4, - "nbformat_minor": 2 -} From 5059bf592cd31e94aa86401e1c4f6d99e98769cc Mon Sep 17 00:00:00 2001 From: Maria Bader Date: Thu, 20 Jun 2024 20:16:31 +0200 Subject: [PATCH 2/4] add google colab utils --- 1_workshop_tools.ipynb | 9 +++++++++ 2_workshop_agents.ipynb | 9 +++++++++ 3_workshop_advanced.ipynb | 9 +++++++++ helper_functions/colab_utils.txt | 8 ++++++++ 4 files changed, 35 insertions(+) create mode 100644 helper_functions/colab_utils.txt diff --git a/1_workshop_tools.ipynb b/1_workshop_tools.ipynb index c1565a9..46a9014 100644 --- a/1_workshop_tools.ipynb +++ b/1_workshop_tools.ipynb @@ -59,6 +59,15 @@ "**Reminder:** Make sure to update `helper_functions/keys.py` based on keys in [privatebin](https://privatebin.molops.io/?a6459e88fa282c28#DsFZvkZSiuPcNQzNXvmtvmTozihhaf1hQdqBCd7r3q5s)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ONLY IF YOU USE GOOGLE COLAB: copy code from helper_functions/colab_utils.txt here and compile" + ] + }, { "cell_type": "code", "execution_count": 2, diff --git a/2_workshop_agents.ipynb b/2_workshop_agents.ipynb index 6471509..3518515 100644 --- a/2_workshop_agents.ipynb +++ b/2_workshop_agents.ipynb @@ -28,6 +28,15 @@ "5. [Exercise 4 : Optimize the agent prompt](#4)\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ONLY IF YOU USE GOOGLE COLAB: copy code from helper_functions/colab_utils.txt here and compile" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/3_workshop_advanced.ipynb b/3_workshop_advanced.ipynb index 4dc3502..5041560 100644 --- a/3_workshop_advanced.ipynb +++ b/3_workshop_advanced.ipynb @@ -28,6 +28,15 @@ "3. [Exercise 3: Understanding the agent while-loop](#3)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ONLY IF YOU USE GOOGLE COLAB: copy code from helper_functions/colab_utils.txt here and compile" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/helper_functions/colab_utils.txt b/helper_functions/colab_utils.txt new file mode 100644 index 0000000..f690e24 --- /dev/null +++ b/helper_functions/colab_utils.txt @@ -0,0 +1,8 @@ +!mkdir helper_functions/ +!mkdir images/ +!curl https://raw.githubusercontent.com/mkmbader/intro-agents-june2024/main/requirements.txt > requirements.txt +!curl https://raw.githubusercontent.com/mkmbader/intro-agents-june2024/main/helper_functions/helper_functions.py > helper_functions/helper_functions.py +!curl https://raw.githubusercontent.com/mkmbader/intro-agents-june2024/main/helper_functions/keys.py > helper_functions/keys.py +!curl https://raw.githubusercontent.com/mkmbader/intro-agents-june2024/main/helper_functions/tools.py > helper_functions/tools.py + +!pip install -r requirements.txt \ No newline at end of file From a0615e8a258edbd563fbca4537dbca81e3ba1b3c Mon Sep 17 00:00:00 2001 From: mkmbader <66826491+mkmbader@users.noreply.github.com> Date: Sat, 22 Jun 2024 16:31:28 +0200 Subject: [PATCH 3/4] Apply suggestions from code review fix typos Co-authored-by: G. Caglia <134130559+gCaglia@users.noreply.github.com> --- 1_workshop_tools.ipynb | 4 ++-- 2_workshop_agents.ipynb | 8 ++++---- 3_workshop_advanced.ipynb | 2 +- helper_functions/tools.py | 2 +- solution/solution_tools.ipynb | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/1_workshop_tools.ipynb b/1_workshop_tools.ipynb index 46a9014..b1040ac 100644 --- a/1_workshop_tools.ipynb +++ b/1_workshop_tools.ipynb @@ -178,7 +178,7 @@ "* The definition of the **input parameters**\n", "* The tool **description**\n", "\n", - "The tool description is especially important, since this is what the agent will use to make the deicion if this tool should be used.\n", + "The tool description is especially important, since this is what the agent will use to make the decision if this tool should be used.\n", "\n", "Below you see the wikipedia tool, built from the basic elements described above:" ] @@ -298,7 +298,7 @@ "Let's do something even more fun. As we previously saw, we can utilize APIs to build tools. Thinking about APIs, one of the biggest collection of models are available via APIs on HuggingFace. So, how about we try to utilize this. \n", "\n", "#### Exercise 2 (b): Build your own Image tool \n", - "The goal is to build a tool that generates an image based on a given prompt. **That means that later when you can build the Agent you can have an LLM that only outputs text, but also images!** \n", + "The goal is to build a tool that generates an image based on a given prompt. **That means that later when you can build the Agent you can have an LLM that not only outputs text, but also images!** \n", "\n", "To develop this, you can make use of `mobius`, text-to-image model available on HuggingFace. We provided a HuggingFace token (that you loaded in the start). \n", "\n", diff --git a/2_workshop_agents.ipynb b/2_workshop_agents.ipynb index 3518515..b57c069 100644 --- a/2_workshop_agents.ipynb +++ b/2_workshop_agents.ipynb @@ -71,7 +71,7 @@ "- **LLM**: A pre-trained LLM.\n", "- **List of tools**: List of tools that give additional functionality to the LLM.\n", "\n", - "One use case of LLM Agents is to make it possible to have LLMs with access to real time information, like the current weather. Namely, when a prompt is called, agents have an LLM and Tools at their disposal. If no tool can be found to help in a answering the question, the agent tries to answer using the raw LLM. E.g. for a given prompt \"What is the **usual** temperature in Amsterdam in summer?\", an LLM will likely already have knowledge. However, a prompt \"What is the **current** temperature in Amsterdam?\", a weather API would be a better source of information, and in this case the Agent will decide to use the information from a weather tool. If such a tool is not available to the agent, the agent will respond that the requested information is not available. \n", + "One use case of LLM Agents is to make it possible to have LLMs with access to real time information, like the current weather. Namely, when a prompt is called, agents have an LLM and Tools at their disposal. If no tool can be found to help in answering the question, the agent tries to answer using the raw LLM. E.g. for a given prompt \"What is the **usual** temperature in Amsterdam in summer?\", an LLM will likely already have knowledge. However, for a prompt \"What is the **current** temperature in Amsterdam?\", a weather API would be a better source of information, and in this case the Agent will decide to use the information from a weather tool. If such a tool is not available to the agent, the agent will respond that the requested information is not available. \n", "\n", "So, basically, you can think of Agents as usual LLMs but with more \"skills\". Cool, right?\n", "\n", @@ -97,7 +97,7 @@ " MessagesPlaceholder(variable_name=\"agent_scratchpad\")\n", "])\n", "\n", - "# Define the agent (load the LLM and the list of tools)\n", + "# Define the agent (load the LLM and the list of tools)\n", "agent = create_tool_calling_agent(llm = llm, tools = tools, prompt = prompt)\n", "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n", "\n", @@ -152,7 +152,7 @@ "metadata": {}, "outputs": [], "source": [ - "question_3 = \"What should I visit in Amsterdam? Show me an photo\"\n", + "question_3 = \"What should I visit in Amsterdam? Show me a photo\"\n", "\n", "print(f\"Question 1: {question_3}\")\n", "agent_executor.invoke({\"input\": question_3})" @@ -270,7 +270,7 @@ "# Component 1 (Tools): Load Tools from Exercise 1\n", "tools = tools \n", "\n", - "# Component 2 (LLM): Load LLM form Exercise 1\n", + "# Component 2 (LLM): Load LLM from Exercise 1\n", "llm = llm\n", "\n", "# Component 3 (Prompt): Create your own prompt to instruct the Agent about its purpose.\n", diff --git a/3_workshop_advanced.ipynb b/3_workshop_advanced.ipynb index 5041560..acebfbf 100644 --- a/3_workshop_advanced.ipynb +++ b/3_workshop_advanced.ipynb @@ -12,7 +12,7 @@ "\n", "**The goal** \n", "\n", - "With this notebook you will see what an agent is under the hood, and you can build it based on the LLM output\n", + "With this notebook you will see what an agent is under the hood, and you can build it based on the LLM output.\n", "\n", "🌟 So ... let us begin! " ] diff --git a/helper_functions/tools.py b/helper_functions/tools.py index ec02b48..1d76169 100644 --- a/helper_functions/tools.py +++ b/helper_functions/tools.py @@ -111,7 +111,7 @@ class ImageInput(BaseModel): # the tool description images_tool_description: str = ( - "Genrate an image based on the input text and return its path" + "Generate an image based on the input text and return its path" ) # fuse the function, input parameters and description into a tool. diff --git a/solution/solution_tools.ipynb b/solution/solution_tools.ipynb index 0d4423e..d4cf88b 100644 --- a/solution/solution_tools.ipynb +++ b/solution/solution_tools.ipynb @@ -307,7 +307,7 @@ "\n", "# the tool description\n", "images_tool_description: str = (\n", - " \"Genrate an image based on the input text and return its path\"\n", + " \"Generate an image based on the input text and return its path\"\n", " )\n", "\n", "# fuse the function, input parameters and description into a tool. \n", From a39cb736b346a99ff507aa3a01ab207fd35408b2 Mon Sep 17 00:00:00 2001 From: Maria Bader Date: Sat, 22 Jun 2024 16:38:27 +0200 Subject: [PATCH 4/4] feedback --- 1_workshop_tools.ipynb | 2 +- requirements.txt | 3 ++- solution/requirements.txt | 6 ------ 3 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 solution/requirements.txt diff --git a/1_workshop_tools.ipynb b/1_workshop_tools.ipynb index b1040ac..e5dc01e 100644 --- a/1_workshop_tools.ipynb +++ b/1_workshop_tools.ipynb @@ -28,7 +28,7 @@ "\n", "You decide to build a tool that helps you get information on holiday locations. For example, you would like to to find out how big a specific city is, what sights are there to see, how the weather there is, and you would like to get a drawing of that place, to get a first impression. Because who does not like art? \n", "\n", - "You will implement this through a sentinent LLM agent, who has access to\n", + "You will implement this through an LLM agent, who has access to\n", "* the wikipedia API,\n", "* a weather API,\n", "* can generate images by using a HuggingFace API. " diff --git a/requirements.txt b/requirements.txt index d9954b2..42d1b47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ langchain_openai langchain_community wikipedia langchainhub -pillow \ No newline at end of file +pillow +jupyter \ No newline at end of file diff --git a/solution/requirements.txt b/solution/requirements.txt deleted file mode 100644 index d9954b2..0000000 --- a/solution/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -langchain -langchain_openai -langchain_community -wikipedia -langchainhub -pillow \ No newline at end of file