2-notebooks/2-agent_service/6-agents-az-functions.ipynb (326 lines of code) (raw):

{ "cells": [ { "cell_type": "markdown", "id": "5683064f", "metadata": {}, "source": [ "# šŸ Fitness Fun: Azure Functions + AI Agent Tutorial šŸŽ\n", "\n", "In this notebook, we'll explore how to use **Azure Functions** with the **Azure AI Foundry** SDKs (`azure-ai-projects`, `azure-ai-inference`, `azure-ai-evaluation`, `opentelemetry-sdk`, and `azure-core-tracing-opentelemetry`). We'll demonstrate how to:\n", "\n", "1. **Set up** an Azure Function that listens on a storage queue.\n", "2. **Create** an AI Agent that can invoke this function.\n", "3. **Send** a prompt to the agent, which then calls the Azure Function.\n", "4. **Retrieve** the processed result from the output queue.\n", "\n", "All with a fun, health-and-fitness-themed example! We'll keep it whimsical, but remember:\n", "\n", "### āš ļø Important Disclaimer\n", "> **This example is for demonstration purposes only and does not provide genuine medical or health advice.** Always consult a professional for real medical or fitness advice.\n", "\n", "## Prerequisites\n", "1. Azure Subscription.\n", "2. **Azure AI Foundry** project. (You'll need your `PROJECT_CONNECTION_STRING` and `MODEL_DEPLOYMENT_NAME`.)\n", "3. **Azure Functions** environment or local emulator (Azurite), plus storage queue knowledge.\n", "4. **Python 3.8+** with `azure-ai-projects`, `azure-identity`, `opentelemetry-sdk`, and `azure-core-tracing-opentelemetry` installed.\n", "\n", "## Overview\n", "We'll do a high-level sequence of events:\n", "\n", "1. **Azure Function** is set up to read messages from an **input queue** and write responses to an **output queue**.\n", "2. **AI Agent** is created with an `AzureFunctionTool` that references these queues.\n", "3. **User** provides a question or command; the agent decides whether or not to call the function.\n", "4. The agent sends a message to the **input queue**, which triggers the function.\n", "5. **Azure Function** processes the message, sends back a response to the **output queue**.\n", "6. The agent picks up the response from the output queue.\n", "7. The **User** sees the final answer from the agent.\n", "\n", "<img src=\"./seq-diagrams/6-az-function.png\" width=\"30%\"/>\n" ] }, { "cell_type": "markdown", "id": "104a0796", "metadata": {}, "source": [ "## 1. Azure Function Setup (Example)\n", "Below is a snippet of how you'd implement the Azure Function that receives a message from the **input queue** and posts a result to the **output queue**.\n", "\n", "You can adapt this code to a local or cloud Azure Functions environment. The function's real logic can be anything – let's pretend it returns a comedic \"foo-based\" answer or some silly \"fitness advice\" snippet for demonstration. \n", "\n", "```python\n", "# This code might live in your Azure Functions project in a file named: __init__.py\n", "# or similar.\n", "import os\n", "import json\n", "import logging\n", "import azure.functions as func\n", "from azure.storage.queue import QueueClient\n", "from azure.core.pipeline.policies import BinaryBase64EncodePolicy, BinaryBase64DecodePolicy\n", "from azure.identity import DefaultAzureCredential\n", "\n", "app = func.FunctionApp()\n", "\n", "@app.function_name(name=\"FooReply\")\n", "@app.queue_trigger(\n", " arg_name=\"inmsg\",\n", " queue_name=\"azure-function-foo-input\",\n", " connection=\"STORAGE_SERVICE_ENDPOINT\" # or connection string setting name\n", ")\n", "def run_foo(inmsg: func.QueueMessage) -> None:\n", " logging.info(\"Azure Function triggered with a queue item.\")\n", "\n", " # This is the queue for output\n", " out_queue = QueueClient(\n", " os.environ[\"STORAGE_SERVICE_ENDPOINT\"], # or read from config\n", " queue_name=\"azure-function-tool-output\",\n", " credential=DefaultAzureCredential(),\n", " message_encode_policy=BinaryBase64EncodePolicy(),\n", " message_decode_policy=BinaryBase64DecodePolicy()\n", " )\n", "\n", " # Parse the function call payload, e.g. { \"query\": \"Hello?\", \"outputqueueuri\":\"...\"}\n", " payload = json.loads(inmsg.get_body().decode('utf-8'))\n", " user_query = payload.get(\"query\", \"\")\n", "\n", " # Example: We'll return a comedic 'Foo says: <some witty line>'\n", " result_message = {\n", " \"FooReply\": f\"This is Foo, responding to: {user_query}! Stay strong šŸ’Ŗ!\",\n", " \"CorrelationId\": payload.get(\"CorrelationId\", \"\")\n", " }\n", "\n", " # Put the result on the output queue\n", " out_queue.send_message(json.dumps(result_message).encode('utf-8'))\n", " logging.info(f\"Sent message: {result_message}\")\n", "```\n", "\n", "### Notes\n", "- The input queue name is `azure-function-foo-input`.\n", "- The output queue name is `azure-function-tool-output`.\n", "- We used environment variables like `STORAGE_SERVICE_ENDPOINT` for the queue storage endpoint.\n" ] }, { "cell_type": "markdown", "id": "94446a72", "metadata": {}, "source": [ "## 2. Notebook Setup\n", "Now let's switch back to this notebook environment. We'll:\n", "1. Import libraries.\n", "2. Initialize `AIProjectClient`.\n", "3. Create the Azure Function tool definition and the Agent.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "b719c505", "metadata": {}, "outputs": [], "source": [ "# We'll do our standard imports\n", "import os\n", "import time\n", "from pathlib import Path\n", "from dotenv import load_dotenv\n", "\n", "from azure.identity import DefaultAzureCredential\n", "from azure.ai.projects import AIProjectClient\n", "from azure.ai.projects.models import AzureFunctionTool, AzureFunctionStorageQueue, MessageRole\n", "\n", "# Load env variables from .env in parent dir\n", "notebook_path = Path().absolute()\n", "parent_dir = notebook_path.parent\n", "load_dotenv(parent_dir / '.env')\n", "\n", "# Create AI Project Client\n", "try:\n", " project_client = AIProjectClient.from_connection_string(\n", " credential=DefaultAzureCredential(exclude_managed_identity_credential=True, exclude_environment_credential=True),\n", " conn_str=os.environ[\"PROJECT_CONNECTION_STRING\"],\n", " )\n", " print(\"āœ… Successfully initialized AIProjectClient\")\n", "except Exception as e:\n", " print(f\"āŒ Error initializing AIProjectClient: {e}\")" ] }, { "cell_type": "markdown", "id": "9c919c6f", "metadata": {}, "source": [ "### Create Agent with Azure Function Tool\n", "We'll define a tool that references our function name (`foo` or `FooReply` from the sample) and the input + output queues. In this example, we'll store the queue endpoint in an env variable called `STORAGE_SERVICE_ENDPOINT`.\n", "\n", "You can adapt it to your own naming scheme. The agent instructions tell it to use the function whenever it sees certain keywords, or you could just let it call the function on its own.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "78008ac0", "metadata": {}, "outputs": [], "source": [ "try:\n", " storage_endpoint = os.environ[\"STORAGE_SERVICE_ENDPONT\"] # Notice it's spelled STORAGE_SERVICE_ENDPONT in sample\n", "except KeyError:\n", " print(\"āŒ Please ensure STORAGE_SERVICE_ENDPONT is set in your environment.\")\n", " storage_endpoint = None\n", "\n", "agent = None\n", "if storage_endpoint:\n", " # Create the AzureFunctionTool object\n", " azure_function_tool = AzureFunctionTool(\n", " name=\"foo\",\n", " description=\"Get comedic or silly advice from 'Foo'.\",\n", " parameters={\n", " \"type\": \"object\",\n", " \"properties\": {\n", " \"query\": {\"type\": \"string\", \"description\": \"The question to ask Foo.\"},\n", " \"outputqueueuri\": {\"type\": \"string\", \"description\": \"The output queue URI.\"}\n", " },\n", " },\n", " input_queue=AzureFunctionStorageQueue(\n", " queue_name=\"azure-function-foo-input\",\n", " storage_service_endpoint=storage_endpoint,\n", " ),\n", " output_queue=AzureFunctionStorageQueue(\n", " queue_name=\"azure-function-tool-output\",\n", " storage_service_endpoint=storage_endpoint,\n", " ),\n", " )\n", "\n", " # Construct the agent with the function tool attached\n", " with project_client:\n", " agent = project_client.agents.create_agent(\n", " model=os.environ[\"MODEL_DEPLOYMENT_NAME\"],\n", " name=\"azure-function-agent-foo\",\n", " instructions=(\n", " \"You are a helpful health and fitness support agent.\\n\" \n", " \"If the user says 'What would foo say?' then call the foo function.\\n\" \n", " \"Always specify the outputqueueuri as '\" + storage_endpoint + \"/azure-function-tool-output'.\\n\"\n", " \"Respond with 'Foo says: <response>' after the tool call.\"\n", " ),\n", " tools=azure_function_tool.definitions,\n", " )\n", " print(f\"šŸŽ‰ Created agent, agent ID: {agent.id}\")\n", "else:\n", " print(\"Skipping agent creation, no storage_endpoint.\")" ] }, { "cell_type": "markdown", "id": "7908348f", "metadata": {}, "source": [ "## 3. Test the Agent\n", "Now let's simulate a user message that triggers the function call. We'll create a conversation **thread**, post a user question that includes \"What would foo say?\", then run the agent. \n", "\n", "The Agent Service will place a message on the `azure-function-foo-input` queue. The function will handle it and place a response in `azure-function-tool-output`. The agent will pick that up automatically and produce a final answer.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "6f551bf4", "metadata": {}, "outputs": [], "source": [ "def run_foo_question(user_question: str, agent_id: str):\n", " # 1) Create a new thread\n", " thread = project_client.agents.create_thread()\n", " print(f\"šŸ“ Created thread, thread ID: {thread.id}\")\n", "\n", " # 2) Create a user message\n", " message = project_client.agents.create_message(\n", " thread_id=thread.id,\n", " role=\"user\",\n", " content=user_question\n", " )\n", " print(f\"šŸ’¬ Created user message, ID: {message.id}\")\n", "\n", " # 3) Create and process agent run\n", " run = project_client.agents.create_and_process_run(\n", " thread_id=thread.id,\n", " agent_id=agent_id\n", " )\n", " print(f\"šŸ¤– Run finished with status: {run.status}\")\n", " if run.status == \"failed\":\n", " print(f\"Run failed: {run.last_error}\")\n", "\n", " # 4) Retrieve messages\n", " messages = project_client.agents.list_messages(thread_id=thread.id)\n", " print(\"\\nšŸ—£ļø Conversation:\")\n", " for m in reversed(messages.data): # oldest first\n", " msg_str = \"\"\n", " if m.content:\n", " msg_str = m.content[-1].text.value if len(m.content) > 0 else \"\"\n", " print(f\"{m.role.upper()}: {msg_str}\\n\")\n", "\n", " return thread, run\n", "\n", "# If the agent was created, let's test it!\n", "if agent:\n", " my_thread, my_run = run_foo_question(\n", " user_question=\"What is the best post-workout snack? What would foo say?\",\n", " agent_id=agent.id\n", " )" ] }, { "cell_type": "markdown", "id": "a3157768", "metadata": {}, "source": [ "## 4. Cleanup\n", "We'll remove the agent when done. In real scenarios, you might keep your agent for repeated usage.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "4e125d5b", "metadata": {}, "outputs": [], "source": [ "if agent:\n", " try:\n", " project_client.agents.delete_agent(agent.id)\n", " print(f\"šŸ—‘ļø Deleted agent: {agent.name}\")\n", " except Exception as e:\n", " print(f\"āŒ Error deleting agent: {e}\")" ] }, { "cell_type": "markdown", "id": "17e7ab6d", "metadata": {}, "source": [ "# šŸŽ‰ Congratulations!\n", "You just saw how to combine **Azure Functions** with **AI Agent Service** to create a dynamic, queue-driven workflow. In this whimsical example, your function returned comedic \"Foo says...\" lines, but in real applications, you can harness the power of Azure Functions to run anything from **database lookups** to **complex calculations**, returning the result seamlessly to your AI agent.\n", "\n", "## Next Steps\n", "- **Add OpenTelemetry** to gather end-to-end tracing across your function and agent.\n", "- Incorporate an **evaluation** pipeline with `azure-ai-evaluation` to measure how well your agent + function workflow addresses user queries.\n", "- Explore **parallel function calls** or more advanced logic in your Azure Functions.\n", "\n", "Happy coding and stay fit! 🤸" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }