2-notebooks/2-agent_service/5-agents-aisearch.ipynb (541 lines of code) (raw):
{
"cells": [
{
"cell_type": "markdown",
"id": "4b6569b8",
"metadata": {},
"source": [
"# 🏋️ AI Search + Agent Service: Fitness-Fun Example 🤸\n",
"\n",
"Welcome to our **AI Search + AI Agent** tutorial, where we'll:\n",
"\n",
"1. **Create** an Azure AI Search index with some fitness-oriented sample data\n",
"2. **Demonstrate** how to connect that index to an Agent via the `AzureAISearchTool`\n",
"3. **Show** how to query the Agent for health and fitness info in a fun scenario (with disclaimers!)\n",
"\n",
"## 🏥 Health & Fitness Disclaimer\n",
"> **This notebook is for general demonstration and entertainment purposes, NOT a substitute for professional medical advice.**\n",
"> Always seek the advice of certified health professionals.\n",
"\n",
"## Prerequisites\n",
"1. Complete Agent basics notebook - [1-basics.ipynb](1-basics.ipynb)\n",
"2. An **Azure AI Search** resource (formerly \"Cognitive Search\"), provisioned in your Azure AI Foundry project.\n",
"\n",
"## High-Level Flow\n",
"We'll do the following:\n",
"1. **Create** an AI Search index programmatically with sample fitness data.\n",
"2. **Upload** documents (fitness items) to the index.\n",
"3. **Create** an Agent that references our new index using `AzureAISearchTool`.\n",
"4. **Run queries** to see how it fetches from the index.\n",
" \n",
" <img src=\"./seq-diagrams/5-ai-search.png\" width=\"30%\"/>\n"
]
},
{
"cell_type": "markdown",
"id": "11b27cec",
"metadata": {},
"source": [
"## 1. Create & Populate Azure AI Search Index\n",
"We'll create a minimal index called `myfitnessindex`, containing a few example items.\n",
"Make sure to set your environment variables for `SEARCH_ENDPOINT` and `SEARCH_API_KEY`. We'll use the `azure.search.documents.indexes` classes to manage the index schema. We'll also upload some sample data.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b555e42d",
"metadata": {},
"outputs": [],
"source": [
"# Import required Azure libraries\n",
"import os\n",
"from azure.core.credentials import AzureKeyCredential # For authentication\n",
"from azure.search.documents.indexes import SearchIndexClient # For managing search indexes\n",
"from azure.search.documents.indexes.models import SearchIndex, SimpleField, SearchFieldDataType, SearchableField # Index schema components\n",
"from azure.search.documents import SearchClient # For document operations (upload/search)\n",
"from azure.identity import DefaultAzureCredential # For Azure authentication\n",
"from azure.ai.projects import AIProjectClient # To access project resources\n",
"from azure.ai.projects.models import ConnectionType # Enum for connection types\n",
"\n",
"# First, initialize the AI Project client which gives us access to project resources\n",
"# This uses DefaultAzureCredential for authentication and the project connection string\n",
"project_client = AIProjectClient.from_connection_string(\n",
" credential=DefaultAzureCredential(),\n",
" conn_str=os.environ[\"PROJECT_CONNECTION_STRING\"]\n",
")\n",
"\n",
"# Get the Azure AI Search connection details from our project\n",
"# This includes endpoint URL and API key needed to access the search service\n",
"search_conn = project_client.connections.get_default(\n",
" connection_type=ConnectionType.AZURE_AI_SEARCH, \n",
" include_credentials=True\n",
")\n",
"if not search_conn:\n",
" raise RuntimeError(\"❌ No default Azure AI Search connection found in your project.\")\n",
"\n",
"# Name of our search index - this is where our fitness data will be stored\n",
"index_name = \"myfitnessindex\"\n",
"\n",
"try:\n",
" # Create a SearchIndexClient - this is used for managing the index itself (create/update/delete)\n",
" credential = AzureKeyCredential(search_conn.key)\n",
" index_client = SearchIndexClient(endpoint=search_conn.endpoint_url, credential=credential)\n",
" print(\"✅ Created SearchIndexClient from project_client connection\")\n",
" \n",
" # Create a SearchClient - this is used for document operations (upload/search/delete documents)\n",
" # We'll use this later to add our fitness items to the index\n",
" search_client = SearchClient(\n",
" endpoint=search_conn.endpoint_url,\n",
" index_name=index_name,\n",
" credential=credential\n",
" )\n",
" print(\"✅ Created SearchClient for document operations\")\n",
" \n",
"except Exception as e:\n",
" print(f\"❌ Error creating search clients: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "a8c418b5",
"metadata": {},
"source": [
"**Define the index** schema with a `FitnessItemID` key and a few fields to store product info.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eed5e598",
"metadata": {},
"outputs": [],
"source": [
"def create_fitness_index():\n",
" # Define the fields (columns) for our search index\n",
" # Each field has specific attributes that control how it can be used in searches:\n",
" fields = [\n",
" # Primary key field - must be unique for each document\n",
" SimpleField(name=\"FitnessItemID\", type=SearchFieldDataType.String, key=True),\n",
" \n",
" # Name field - SearchableField means we can do full-text search on it\n",
" # filterable=True lets us filter results by name\n",
" SearchableField(name=\"Name\", type=SearchFieldDataType.String, filterable=True),\n",
" \n",
" # Category field - SearchableField for text search\n",
" # filterable=True lets us filter by category\n",
" # facetable=True enables category grouping in results\n",
" SearchableField(name=\"Category\", type=SearchFieldDataType.String, filterable=True, facetable=True),\n",
" \n",
" # Price field - SimpleField for numeric values\n",
" # filterable=True enables price range filters\n",
" # sortable=True lets us sort by price\n",
" # facetable=True enables price range grouping\n",
" SimpleField(name=\"Price\", type=SearchFieldDataType.Double, filterable=True, sortable=True, facetable=True),\n",
" \n",
" # Description field - SearchableField for full-text search on product descriptions\n",
" SearchableField(name=\"Description\", type=SearchFieldDataType.String)\n",
" ]\n",
"\n",
" # Create an index definition with our fields\n",
" index = SearchIndex(name=index_name, fields=fields)\n",
"\n",
" # Check if index already exists - if so, delete it to start fresh\n",
" # This is useful during development but be careful in production!\n",
" if index_name in [x.name for x in index_client.list_indexes()]:\n",
" index_client.delete_index(index_name)\n",
" print(f\"🗑️ Deleted existing index: {index_name}\")\n",
"\n",
" # Create the new index with our schema\n",
" created = index_client.create_index(index)\n",
" print(f\"🎉 Created index: {created.name}\")\n",
"\n",
"# Execute the function to create our search index\n",
"create_fitness_index()"
]
},
{
"cell_type": "markdown",
"id": "17035c71",
"metadata": {},
"source": [
"**Upload some sample documents** to `myfitnessindex`. We'll add a few items for demonstration.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9af38671",
"metadata": {},
"outputs": [],
"source": [
"def upload_fitness_docs():\n",
" # Create a SearchClient to interact with our search index\n",
" # This uses the connection details (endpoint, key) we configured earlier\n",
" search_client = SearchClient(\n",
" endpoint=search_conn.endpoint_url,\n",
" index_name=index_name,\n",
" credential=AzureKeyCredential(search_conn.key)\n",
" )\n",
"\n",
" # Define sample documents that match our index schema\n",
" # Each document must have:\n",
" # - FitnessItemID (unique identifier)\n",
" # - Name (searchable product name) \n",
" # - Category (searchable and facetable for filtering/grouping)\n",
" # - Price (numeric field for sorting and filtering)\n",
" # - Description (searchable product details)\n",
" sample_docs = [\n",
" {\n",
" \"FitnessItemID\": \"1\",\n",
" \"Name\": \"Adjustable Dumbbell\",\n",
" \"Category\": \"Strength\", \n",
" \"Price\": 59.99,\n",
" \"Description\": \"A compact, adjustable weight for targeted muscle workouts.\"\n",
" },\n",
" {\n",
" \"FitnessItemID\": \"2\",\n",
" \"Name\": \"Yoga Mat\",\n",
" \"Category\": \"Flexibility\",\n",
" \"Price\": 25.0,\n",
" \"Description\": \"Non-slip mat designed for yoga, Pilates, and other exercises.\"\n",
" },\n",
" {\n",
" \"FitnessItemID\": \"3\",\n",
" \"Name\": \"Treadmill\",\n",
" \"Category\": \"Cardio\",\n",
" \"Price\": 499.0,\n",
" \"Description\": \"A sturdy treadmill with adjustable speed and incline settings.\"\n",
" },\n",
" {\n",
" \"FitnessItemID\": \"4\",\n",
" \"Name\": \"Resistance Bands\",\n",
" \"Category\": \"Strength\",\n",
" \"Price\": 15.0,\n",
" \"Description\": \"Set of colorful bands for light to moderate resistance workouts.\"\n",
" }\n",
" ]\n",
"\n",
" # Upload all documents to the search index in a single batch operation\n",
" # The search service will index these documents, making them searchable\n",
" # based on the field types we defined in our index schema\n",
" result = search_client.upload_documents(documents=sample_docs)\n",
" print(f\"🚀 Upload result: {result}\")\n",
"\n",
"# Call the function to upload the documents\n",
"upload_fitness_docs()\n",
"print(\"✅ Documents uploaded to search index\")\n"
]
},
{
"cell_type": "markdown",
"id": "d95b1386",
"metadata": {},
"source": [
"### Verify the documents via a basic query\n",
"Let's do a quick search for **Strength** items.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2af93084",
"metadata": {},
"outputs": [],
"source": [
"# Let's verify our index by performing a basic search\n",
"# 1. First create a SearchClient using our connection details\n",
"# - endpoint_url: The URL of our search service\n",
"# - index_name: The name of the index we created earlier\n",
"# - key: The admin key to authenticate our requests\n",
"search_client = SearchClient(\n",
" endpoint=search_conn.endpoint_url,\n",
" index_name=index_name,\n",
" credential=AzureKeyCredential(search_conn.key)\n",
")\n",
"\n",
"# 2. Perform a simple search query:\n",
"# - search_text=\"Strength\": Look for documents containing \"Strength\"\n",
"# - filter=None: No additional filtering\n",
"# - top=10: Return up to 10 matching documents\n",
"results = search_client.search(search_text=\"Strength\", filter=None, top=10)\n",
"\n",
"# 3. Print each matching document\n",
"print(\"🔍 Search results for 'Strength':\")\n",
"print(\"-\" * 50)\n",
"found_items = False\n",
"for doc in results:\n",
" found_items = True\n",
" # The doc is already a dictionary, no need for to_dict()\n",
" print(f\"Name: {doc['Name']}\")\n",
" print(f\"Category: {doc['Category']}\")\n",
" print(f\"Price: ${doc['Price']:.2f}\")\n",
" print(f\"Description: {doc['Description']}\")\n",
" print(\"-\" * 50)\n",
"\n",
"if not found_items:\n",
" print(\"No matching items found.\")"
]
},
{
"cell_type": "markdown",
"id": "ed67b2f4",
"metadata": {},
"source": [
"## 2. Create Agent With AI Search Tool\n",
"We'll create a new agent and attach an `AzureAISearchTool` referencing **myfitnessindex**.\n",
"In your environment, you need:\n",
"- `PROJECT_CONNECTION_STRING` - from your AI Foundry project overview\n",
"- `MODEL_DEPLOYMENT_NAME` - from the deployed model name\n",
"\n",
"Let's initialize the `AIProjectClient` with `DefaultAzureCredential`.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c31d0e8",
"metadata": {},
"outputs": [],
"source": [
"# Import required libraries:\n",
"# - os: For accessing environment variables\n",
"# - DefaultAzureCredential: Azure's authentication mechanism\n",
"# - AIProjectClient: Main client for interacting with AI Projects\n",
"# - AzureAISearchTool & ConnectionType: Used to configure search capabilities\n",
"import os\n",
"from azure.identity import DefaultAzureCredential\n",
"from azure.ai.projects import AIProjectClient\n",
"from azure.ai.projects.models import AzureAISearchTool, ConnectionType\n",
"\n",
"# Initialize the AI Project Client which we'll use to:\n",
"# 1. Connect to our Azure AI project\n",
"# 2. Create agents with search capabilities\n",
"# 3. Manage project resources\n",
"try:\n",
" project_client = AIProjectClient.from_connection_string(\n",
" # Use Azure's default authentication method\n",
" credential=DefaultAzureCredential(),\n",
" # Connect using the project connection string from environment variables\n",
" conn_str=os.environ[\"PROJECT_CONNECTION_STRING\"],\n",
" )\n",
" print(\"✅ Successfully initialized AIProjectClient\")\n",
"except Exception as e:\n",
" print(f\"❌ Error initializing project client: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "a4988b6c",
"metadata": {},
"source": [
"### Find (or create) the Azure AI Search connection in your Foundry project\n",
"We'll now use `project_client.connections.get_default(...)` to retrieve the default Azure AI Search connection, **including** credentials.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9de13ece",
"metadata": {},
"outputs": [],
"source": [
"# Try to get the default Azure AI Search connection from our project\n",
"# - Azure AI Search (formerly Cognitive Search) is a cloud search service that helps \n",
"# us add search capabilities to our applications\n",
"# - The connection contains endpoint and credential information needed to access the search service\n",
"search_conn = project_client.connections.get_default(\n",
" # Specify we want an Azure AI Search connection type\n",
" connection_type=ConnectionType.AZURE_AI_SEARCH,\n",
" # include_credentials=True means we'll get the full connection info including auth keys\n",
" include_credentials=True\n",
")\n",
"\n",
"# Check if we found a connection\n",
"if not search_conn:\n",
" print(\"❌ No default Azure AI Search connection found in your project.\")\n",
"else:\n",
" # If found, print the connection details\n",
" # - The connection ID is a unique identifier for this connection in our project\n",
" # - The endpoint URL is where our search service is hosted\n",
" print(f\"Found default Azure AI Search connection ID: {search_conn.id}\")\n",
" print(f\"Index endpoint: {search_conn.endpoint_url}\")"
]
},
{
"cell_type": "markdown",
"id": "e921f6c7",
"metadata": {},
"source": [
"### Create the Agent with `AzureAISearchTool`\n",
"We'll attach the tool, specifying the index name we created.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e2fe256",
"metadata": {},
"outputs": [],
"source": [
"# Get the model deployment name from environment variables\n",
"# This is the Azure OpenAI model we'll use for our agent\n",
"model_name = os.environ.get(\"MODEL_DEPLOYMENT_NAME\")\n",
"agent = None\n",
"\n",
"if search_conn:\n",
" # Create an Azure AI Search tool that will allow our agent to search the fitness equipment index\n",
" # - The tool needs the connection ID we got earlier to authenticate\n",
" # - index_name specifies which search index to query (we created myfitnessindex earlier)\n",
" ai_search_tool = AzureAISearchTool(\n",
" index_connection_id=search_conn.id,\n",
" index_name=index_name\n",
" )\n",
"\n",
" # Create an AI agent that can understand natural language and search our index\n",
" # - The agent uses our Azure OpenAI model for natural language understanding\n",
" # - We give it instructions to act as a fitness shopping assistant\n",
" # - We attach the search tool so it can look up products\n",
" # - tool_resources provides the connection details the tool needs\n",
" agent = project_client.agents.create_agent(\n",
" model=model_name,\n",
" name=\"fitness-agent-search\",\n",
" instructions=\"\"\"\n",
" You are a Fitness Shopping Assistant. You help users find items, but always disclaim not to provide medical advice.\n",
" \"\"\",\n",
" tools=ai_search_tool.definitions,\n",
" tool_resources=ai_search_tool.resources,\n",
" headers={\"x-ms-enable-preview\": \"true\"}, # Enable preview features\n",
" )\n",
" print(f\"🎉 Created agent, ID: {agent.id}\")"
]
},
{
"cell_type": "markdown",
"id": "704445fc",
"metadata": {},
"source": [
"## 3. Run a Conversation with the Agent\n",
"We'll open a new thread, post a question, and let the agent search the index for relevant items."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9ba65c9b",
"metadata": {},
"outputs": [],
"source": [
"def run_agent_query(question: str):\n",
" # Step 1: Create a new conversation thread\n",
" # In Azure AI Agent service, conversations happen in threads, similar to chat conversations\n",
" # Each thread can contain multiple back-and-forth messages\n",
" thread = project_client.agents.create_thread()\n",
" print(f\"📝 Created thread, ID: {thread.id}\")\n",
"\n",
" # Step 2: Add the user's question as a message in the thread\n",
" # Messages have roles (\"user\" or \"assistant\") and content (the actual text)\n",
" message = project_client.agents.create_message(\n",
" thread_id=thread.id,\n",
" role=\"user\",\n",
" content=question\n",
" )\n",
" print(f\"💬 Created user message, ID: {message.id}\")\n",
"\n",
" # Step 3: Create and start an agent run\n",
" # This tells the agent to:\n",
" # - Read the user's message\n",
" # - Use its AI Search tool to find relevant products\n",
" # - Generate a helpful response\n",
" run = project_client.agents.create_and_process_run(\n",
" thread_id=thread.id,\n",
" agent_id=agent.id\n",
" )\n",
" print(f\"🤖 Agent run status: {run.status}\")\n",
"\n",
" # Check for any errors during the agent's processing\n",
" if run.last_error:\n",
" print(\"⚠️ Run error:\", run.last_error)\n",
"\n",
" # Step 4: Get the agent's response\n",
" # Retrieve all messages and find the most recent assistant response\n",
" # The response might contain multiple content blocks (text, images, etc.)\n",
" msg_list = project_client.agents.list_messages(thread_id=thread.id)\n",
" for m in reversed(msg_list.data):\n",
" if m.role == \"assistant\" and m.content:\n",
" print(\"\\nAssistant says:\")\n",
" for c in m.content:\n",
" if hasattr(c, \"text\"):\n",
" print(c.text.value)\n",
" break\n",
"\n",
"# Try out our agent with two example queries:\n",
"# 1. A general question about strength training equipment\n",
"# 2. A specific request for cardio equipment with a price constraint\n",
"if agent:\n",
" run_agent_query(\"Which items are good for strength training?\")\n",
" run_agent_query(\"I need something for cardio under $300, any suggestions?\")"
]
},
{
"cell_type": "markdown",
"id": "0292c370",
"metadata": {},
"source": [
"## 4. Cleanup\n",
"We'll clean up the agent. (In production, you might want to keep it!)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "285fdc95",
"metadata": {},
"outputs": [],
"source": [
"if agent:\n",
" project_client.agents.delete_agent(agent.id)\n",
" print(\"🗑️ Deleted agent\")\n",
"\n",
"index_client.delete_index(index_name)\n",
"print(f\"🗑️ Deleted index {index_name}\")"
]
},
{
"cell_type": "markdown",
"id": "dcf584e3",
"metadata": {},
"source": [
"# 🎉 Congrats!\n",
"You've successfully:\n",
"1. **Created** an Azure AI Search index programmatically.\n",
"2. **Populated** it with sample fitness data.\n",
"3. **Created** an Agent that queries the index using `AzureAISearchTool`.\n",
"4. **Asked** the agent for item recommendations.\n",
"\n",
"Continue exploring how to integrate **OpenTelemetry** or the `azure-ai-evaluation` library for advanced tracing and evaluation capabilities. Have fun, and stay fit! 🏆"
]
}
],
"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.11.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}