{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## The first big project - Professionally You!\n", "\n", "### And, Tool use.\n", "\n", "### But first: introducing Pushover\n", "\n", "Pushover 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://pushover.net/ and click 'Login or Signup' on the top right to sign up for a free account, and create your API keys.\n", "\n", "Once you've signed up, on the home screen, click \"Create an Application/API Token\", and give it any name (like Agents) and click Create Application.\n", "\n", "Then add 2 lines to your `.env` file:\n", "\n", "PUSHOVER_USER=_put the key that's on the top right of your Pushover home screen and probably starts with a u_ \n", "PUSHOVER_TOKEN=_put the key when you click into your new application called Agents (or whatever) and probably starts with an a_\n", "\n", "Remember to save your `.env` file, and run `load_dotenv(override=True)` after saving, to set your environment variables.\n", "\n", "Finally, click \"Add Phone, Tablet or Desktop\" to install on your phone." ] }, { "cell_type": "code", "execution_count": 33, "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" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "# The usual start\n", "\n", "load_dotenv(override=True)\n", "deepseek = OpenAI(api_key=os.getenv('DEEPSEEK_API_KEY'), base_url=\"https://api.deepseek.com/v1\")" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Pushover user found and starts with u\n", "Pushover token found and starts with a\n" ] } ], "source": [ "# For pushover\n", "\n", "pushover_user = os.getenv(\"PUSHOVER_USER\")\n", "pushover_token = os.getenv(\"PUSHOVER_TOKEN\")\n", "pushover_url = \"https://api.pushover.net/1/messages.json\"\n", "\n", "if pushover_user:\n", " print(f\"Pushover user found and starts with {pushover_user[0]}\")\n", "else:\n", " print(\"Pushover user not found\")\n", "\n", "if pushover_token:\n", " print(f\"Pushover token found and starts with {pushover_token[0]}\")\n", "else:\n", " print(\"Pushover token not found\")" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "def push(message):\n", " print(f\"Push: {message}\")\n", " payload = {\"user\": pushover_user, \"token\": pushover_token, \"message\": message}\n", " requests.post(pushover_url, data=payload)" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Push: HEY!!\n" ] } ], "source": [ "push(\"HEY!!\")" ] }, { "cell_type": "code", "execution_count": 41, "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": 45, "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": 46, "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": 47, "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": 48, "metadata": {}, "outputs": [], "source": [ "tools = [{\"type\": \"function\", \"function\": record_user_details_json},\n", " {\"type\": \"function\", \"function\": record_unknown_question_json}]" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'type': 'function',\n", " 'function': {'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': {'type': 'object',\n", " 'properties': {'email': {'type': 'string',\n", " 'description': 'The email address of this user'},\n", " 'name': {'type': 'string',\n", " 'description': \"The user's name, if they provided it\"},\n", " 'notes': {'type': 'string',\n", " 'description': \"Any additional information about the conversation that's worth recording to give context\"}},\n", " 'required': ['email'],\n", " 'additionalProperties': False}}},\n", " {'type': 'function',\n", " 'function': {'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': {'type': 'object',\n", " 'properties': {'question': {'type': 'string',\n", " 'description': \"The question that couldn't be answered\"}},\n", " 'required': ['question'],\n", " 'additionalProperties': False}}}]" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tools" ] }, { "cell_type": "code", "execution_count": 50, "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": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Push: Recording this is a really hard question asked that I couldn't answer\n" ] }, { "data": { "text/plain": [ "{'recorded': 'ok'}" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "globals()[\"record_unknown_question\"](\"this is a really hard question\")" ] }, { "cell_type": "code", "execution_count": 52, "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": 53, "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 = \"Pagi\"" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [], "source": [ "system_prompt = (\n", " f\"You are acting as {name}. You are answering questions on {name}'s website, \"\n", " f\"particularly questions related to {name}'s career, background, skills, and professional expertise. \"\n", " f\"Your responsibility is to represent {name} faithfully and consistently, as if you were {name} speaking directly. \"\n", " f\"Highlight {name}'s technical knowledge, career achievements, and ability to orchestrate AI workflows, \"\n", " f\"while also reflecting {name}'s approachable, insightful, and execution focused personality. \"\n", " f\"Always be professional, engaging, and concise. Balance expertise with accessibility. \"\n", " f\"Assume the user may be a potential client, employer, or collaborator, and answer accordingly. \"\n", " f\"On session start, send the Initial Outreach Message below once before answering any question. \"\n", " f\"After that, continue normal chat. \"\n", " f\"\\\\n\\\\n\"\n", " f\"If you don't know the answer to a question, use your record_unknown_question tool to capture it. \"\n", " f\"Never invent details beyond the provided summary and LinkedIn profile. \"\n", " f\"If the user is engaging in casual discussion, respond warmly but always try to steer the conversation \"\n", " f\"towards professional opportunities or getting in touch. Politely ask for their email and record it \"\n", " f\"using the record_user_details tool whenever relevant. \"\n", " f\"\\\\n\\\\n\"\n", " f\"### Guardrails and Style:\\\\n\"\n", " f\"* Represent {name}'s background and expertise accurately using only the provided context.\\\\n\"\n", " f\"* Keep responses clear, structured, and free of jargon unless explained.\\\\n\"\n", " f\"* Do not use hyphens, em dashes, or overcomplicated formatting.\\\\n\"\n", " f\"* Avoid speculative or personal details not included in {{summary}} or {{linkedin}}.\\\\n\"\n", " f\"* Promote responsible, ethical use of technology and AI.\\\\n\"\n", " f\"* End with a professional, engaging tone that invites further interaction.\\\\n\"\n", " f\"\\\\n\\\\n\"\n", " f\"## Summary:\\\\n{summary}\\\\n\\\\n\"\n", " f\"## LinkedIn Profile:\\\\n{linkedin}\\\\n\\\\n\"\n", " f\"## Initial Outreach Message:\\\\n\"\n", " f\"Hello, I am the digital assistant for {name}. I help visitors explore his work in marine engineering and software, and I showcase AI driven solutions he builds. \"\n", " f\"If you have a challenge, tell me your use case, constraints, and timeline. I can outline a lean pilot, integration approach, and next steps. \"\n", " f\"Share an email for a quick follow up and I will record it for {name}.\\\\n\\\\n\"\n", " f\"With this context, please chat with the user, always staying in character as {name}.\"\n", ")\n" ] }, { "cell_type": "code", "execution_count": 55, "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 = deepseek.chat.completions.create(model=\"deepseek-chat\", 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": 56, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* Running on local URL: http://127.0.0.1:7863\n", "* To create a public link, set `share=True` in `launch()`.\n" ] }, { "data": { "text/html": [ "
" ], "text/plain": [ "\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", " | \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", " | \n",
"