{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## The first big project - Professionally You!\n", "\n", "### And, Tool use.\n", "\n", "### But first: introducing Slack\n", "\n", "Slack is a nifty tool for sending Push Notifications to your phone.\n", "\n", "It's super easy to set up and install!\n", "\n", "Simply visit https://api.slack.com and sign up for a free account, and create your new workspace and app.\n", "\n", "1. Create a Slack App:\n", "- Go to the [Slack API portal](https://api.slack.com/apps) and click Create New App.\n", "- Choose From scratch, provide an App Name (e.g., \"CustomerNotifier\"), and select the Slack workspace where you want to - install the app.\n", "- Click Create App.\n", "\n", "2. Add Required Permissions (Scopes):\n", "- Navigate to OAuth & Permissions in the left sidebar of your app’s management page.\n", "- Under Bot Token Scopes, add the chat:write scope to allow your app to post messages. If you need to send direct messages (DMs) to users, also add im:write and users:read to fetch user IDs.\n", "- If you plan to post to specific channels, ensure the app has permissions like channels:write or groups:write for public or private channels, respectively.\n", "\n", "3. Install the App to Your Workspace:\n", "- In the OAuth & Permissions section, click Install to Workspace.\n", "- Authorize the app, selecting the channel where it will post messages (if using incoming webhooks) or granting the necessary permissions.\n", "- After installation, you’ll receive a Bot User OAuth Token (starts with xoxb-). Copy this token, as it will be used for - API authentication. Keep it secure and avoid hardcoding it in your source code.\n", "\n", "(This is so you could choose to organize your push notifications into different apps in the future.)\n", "\n", "4. Create a new private channel in slack App\n", "- Opt to use Private Access\n", "- After creating the private channel, type \"@\" to allow slack default bot to invite the bot into your chat\n", "- Go to \"About\" of your private chat. Copy the channel Id at the bottom\n", "\n", "5. Install slack_sdk==3.35.0 into your env\n", "```\n", "uv pip install slack_sdk==3.35.0\n", "```\n", "\n", "Add to your `.env` file:\n", "```\n", "SLACK_AGENT_CHANNEL_ID=put_your_user_token_here\n", "SLACK_BOT_AGENT_OAUTH_TOKEN=put_the_oidc_token_here\n", "```\n", "\n", "And install the Slack app on your phone." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# imports\n", "\n", "from dotenv import load_dotenv\n", "from openai import OpenAI\n", "import json\n", "import os\n", "import requests\n", "from pypdf import PdfReader\n", "import gradio as gr\n", "from slack_sdk import WebClient\n", "from slack_sdk.errors import SlackApiError" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# The usual start\n", "\n", "load_dotenv(override=True)\n", "openai = OpenAI()" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# For slack\n", "\n", "slack_channel_id:str = str(os.getenv(\"SLACK_AGENT_CHANNEL_ID\"))\n", "slack_oauth_token = os.getenv(\"SLACK_BOT_AGENT_OAUTH_TOKEN\")\n", "slack_client = WebClient(token=slack_oauth_token)\n" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def push(message):\n", " print(f\"Push: {message}\")\n", " response = slack_client.chat_postMessage(\n", " channel=slack_channel_id,\n", " text=message\n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "push(\"HEY!!\")" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "def record_user_details(email, name=\"Name not provided\", notes=\"not provided\"):\n", " push(f\"Recording interest from {name} with email {email} and notes {notes}\")\n", " return {\"recorded\": \"ok\"}" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "def record_unknown_question(question):\n", " push(f\"Recording {question} asked that I couldn't answer\")\n", " return {\"recorded\": \"ok\"}" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "record_user_details_json = {\n", " \"name\": \"record_user_details\",\n", " \"description\": \"Use this tool to record that a user is interested in being in touch and provided an email address\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"email\": {\n", " \"type\": \"string\",\n", " \"description\": \"The email address of this user\"\n", " },\n", " \"name\": {\n", " \"type\": \"string\",\n", " \"description\": \"The user's name, if they provided it\"\n", " }\n", " ,\n", " \"notes\": {\n", " \"type\": \"string\",\n", " \"description\": \"Any additional information about the conversation that's worth recording to give context\"\n", " }\n", " },\n", " \"required\": [\"email\"],\n", " \"additionalProperties\": False\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "record_unknown_question_json = {\n", " \"name\": \"record_unknown_question\",\n", " \"description\": \"Always use this tool to record any question that couldn't be answered as you didn't know the answer\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"question\": {\n", " \"type\": \"string\",\n", " \"description\": \"The question that couldn't be answered\"\n", " },\n", " },\n", " \"required\": [\"question\"],\n", " \"additionalProperties\": False\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "tools = [{\"type\": \"function\", \"function\": record_user_details_json},\n", " {\"type\": \"function\", \"function\": record_unknown_question_json}]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tools" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "# This function can take a list of tool calls, and run them. This is the IF statement!!\n", "\n", "def handle_tool_calls(tool_calls):\n", " results = []\n", " for tool_call in tool_calls:\n", " tool_name = tool_call.function.name\n", " arguments = json.loads(tool_call.function.arguments)\n", " print(f\"Tool called: {tool_name}\", flush=True)\n", "\n", " # THE BIG IF STATEMENT!!!\n", "\n", " if tool_name == \"record_user_details\":\n", " result = record_user_details(**arguments)\n", " elif tool_name == \"record_unknown_question\":\n", " result = record_unknown_question(**arguments)\n", "\n", " results.append({\"role\": \"tool\",\"content\": json.dumps(result),\"tool_call_id\": tool_call.id})\n", " return results" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "globals()[\"record_unknown_question\"](\"this is a really hard question\")" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "# This is a more elegant way that avoids the IF statement.\n", "\n", "def handle_tool_calls(tool_calls):\n", " results = []\n", " for tool_call in tool_calls:\n", " tool_name = tool_call.function.name\n", " arguments = json.loads(tool_call.function.arguments)\n", " print(f\"Tool called: {tool_name}\", flush=True)\n", " tool = globals().get(tool_name)\n", " result = tool(**arguments) if tool else {}\n", " results.append({\"role\": \"tool\",\"content\": json.dumps(result),\"tool_call_id\": tool_call.id})\n", " return results" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "reader = PdfReader(\"me/linkedin.pdf\")\n", "linkedin = \"\"\n", "for page in reader.pages:\n", " text = page.extract_text()\n", " if text:\n", " linkedin += text\n", "\n", "with open(\"me/summary.txt\", \"r\", encoding=\"utf-8\") as f:\n", " summary = f.read()\n", "\n", "name = \"Ed Donner\"" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "system_prompt = f\"You are acting as {name}. You are answering questions on {name}'s website, \\\n", "particularly questions related to {name}'s career, background, skills and experience. \\\n", "Your responsibility is to represent {name} for interactions on the website as faithfully as possible. \\\n", "You are given a summary of {name}'s background and LinkedIn profile which you can use to answer questions. \\\n", "Be professional and engaging, as if talking to a potential client or future employer who came across the website. \\\n", "If you don't know the answer to any question, use your record_unknown_question tool to record the question that you couldn't answer, even if it's about something trivial or unrelated to career. \\\n", "If the user is engaging in discussion, try to steer them towards getting in touch via email; ask for their email and record it using your record_user_details tool. \"\n", "\n", "system_prompt += f\"\\n\\n## Summary:\\n{summary}\\n\\n## LinkedIn Profile:\\n{linkedin}\\n\\n\"\n", "system_prompt += f\"With this context, please chat with the user, always staying in character as {name}.\"\n" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "def chat(message, history):\n", " messages = [{\"role\": \"system\", \"content\": system_prompt}] + history + [{\"role\": \"user\", \"content\": message}]\n", " done = False\n", " while not done:\n", "\n", " # This is the call to the LLM - see that we pass in the tools json\n", "\n", " response = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages, tools=tools)\n", "\n", " finish_reason = response.choices[0].finish_reason\n", " \n", " # If the LLM wants to call a tool, we do that!\n", " \n", " if finish_reason==\"tool_calls\":\n", " message = response.choices[0].message\n", " tool_calls = message.tool_calls\n", " results = handle_tool_calls(tool_calls)\n", " messages.append(message)\n", " messages.extend(results)\n", " else:\n", " done = True\n", " return response.choices[0].message.content" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gr.ChatInterface(chat, type=\"messages\").launch()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## And now for deployment\n", "\n", "This code is in `app.py`\n", "\n", "We will deploy to HuggingFace Spaces. Thank you student Robert M for improving these instructions.\n", "\n", "Before you start: remember to update the files in the \"me\" directory - your LinkedIn profile and summary.txt - so that it talks about you! \n", "Also check that there's no README file within the 1_foundations directory. If there is one, please delete it. The deploy process creates a new README file in this directory for you.\n", "\n", "1. Visit https://huggingface.co and set up an account \n", "2. From the Avatar menu on the top right, choose Access Tokens. Choose \"Create New Token\". Give it WRITE permissions.\n", "3. Take this token and add it to your .env file: `HF_TOKEN=hf_xxx` and see note below if this token doesn't seem to get picked up during deployment \n", "4. From the 1_foundations folder, enter: `uv run gradio deploy` and if for some reason this still wants you to enter your HF token, then interrupt it with ctrl+c and run this instead: `uv run dotenv -f ../.env run -- uv run gradio deploy` which forces your keys to all be set as environment variables \n", "5. Follow its instructions: name it \"career_conversation\", specify app.py, choose cpu-basic as the hardware, say Yes to needing to supply secrets, provide your openai api key, your pushover user and token, and say \"no\" to github actions. \n", "\n", "#### Extra note about the HuggingFace token\n", "\n", "A couple of students have mentioned the HuggingFace doesn't detect their token, even though it's in the .env file. Here are things to try: \n", "1. Restart Cursor \n", "2. Rerun load_dotenv(override=True) and use a new terminal (the + button on the top right of the Terminal) \n", "3. In the Terminal, run this before the gradio deploy: `$env:HF_TOKEN = \"hf_XXXX\"` \n", "Thank you James and Martins for these tips. \n", "\n", "#### More about these secrets:\n", "\n", "If you're confused by what's going on with these secrets: it just wants you to enter the key name and value for each of your secrets -- so you would enter: \n", "`OPENAI_API_KEY` \n", "Followed by: \n", "`sk-proj-...` \n", "\n", "And if you don't want to set secrets this way, or something goes wrong with it, it's no problem - you can change your secrets later: \n", "1. Log in to HuggingFace website \n", "2. Go to your profile screen via the Avatar menu on the top right \n", "3. Select the Space you deployed \n", "4. Click on the Settings wheel on the top right \n", "5. You can scroll down to change your secrets, delete the space, etc.\n", "\n", "#### And now you should be deployed!\n", "\n", "Here is mine: https://huggingface.co/spaces/ed-donner/Career_Conversation\n", "\n", "I just got a push notification that a student asked me how they can become President of their country 😂😂\n", "\n", "For more information on deployment:\n", "\n", "https://www.gradio.app/guides/sharing-your-app#hosting-on-hf-spaces\n", "\n", "To delete your Space in the future: \n", "1. Log in to HuggingFace\n", "2. From the Avatar menu, select your profile\n", "3. Click on the Space itself and select the settings wheel on the top right\n", "4. Scroll to the Delete section at the bottom\n", "5. ALSO: delete the README file that Gradio may have created inside this 1_foundations folder (otherwise it won't ask you the questions the next time you do a gradio deploy)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", "

Exercise

\n", " • First and foremost, deploy this for yourself! It's a real, valuable tool - the future resume..
\n", " • Next, improve the resources - add better context about yourself. If you know RAG, then add a knowledge base about you.
\n", " • Add in more tools! You could have a SQL database with common Q&A that the LLM could read and write from?
\n", " • Bring in the Evaluator from the last lab, and add other Agentic patterns.\n", "
\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", "

Commercial implications

\n", " Aside from the obvious (your career alter-ego) this has business applications in any situation where you need an AI assistant with domain expertise and an ability to interact with the real world.\n", " \n", "
" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.11" } }, "nbformat": 4, "nbformat_minor": 2 }