diff --git a/python/samples/04-hosting/foundry-hosted-agents/README.md b/python/samples/04-hosting/foundry-hosted-agents/README.md index 9ff0030eaa..bb55657564 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/README.md @@ -17,7 +17,8 @@ This directory contains samples that demonstrate how to use hosted [Agent Framew | 7 | [Observability](responses/07_observability/) | A sample demonstrating how to enable observability for the agent deployed to Foundry. | | 8 | [Azure AI Search RAG](responses/08_azure_search_rag/) | An agent with Retrieval Augmented Generation (RAG) capabilities backed by Azure AI Search, grounding answers in documents indexed in a pre-provisioned search index. | | 9 | [Foundry Skills](responses/09_foundry_skills/) | An agent that uploads `SKILL.md` files to the Foundry Skills REST API and downloads them at startup, decoupling tone/policy guidelines from agent code. | -| 10 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. | +| 10 | [Foundry Memory](responses/10_foundry_memory/) | An agent with persistent semantic memory backed by an Azure AI Foundry Memory Store, using `FoundryMemoryProvider` to remember user facts across sessions. | +| 11 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. | ### Invocations API diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/.dockerignore b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/.dockerignore new file mode 100644 index 0000000000..0848068228 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/.dockerignore @@ -0,0 +1,8 @@ +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env +provision_memory_store.py diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/.env.example b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/.env.example new file mode 100644 index 0000000000..41009a5570 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/.env.example @@ -0,0 +1,9 @@ +FOUNDRY_PROJECT_ENDPOINT="..." +AZURE_AI_MODEL_DEPLOYMENT_NAME="..." +# Embedding model deployment (only needed by provision_memory_store.py). +AZURE_OPENAI_EMBEDDING_MODEL="text-embedding-3-small" +# Name of the Foundry Memory Store the agent should read/write to. +FOUNDRY_MEMORY_STORE_NAME="agent_framework_memory" +# Optional scope (e.g., user id) used to isolate memories. Falls back to the +# per-session id when unset, which limits memories to a single session. +FOUNDRY_MEMORY_SCOPE="user_123" diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/Dockerfile b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/Dockerfile new file mode 100644 index 0000000000..0cc939d9b3 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY . user_agent/ +WORKDIR /app/user_agent + +RUN if [ -f requirements.txt ]; then \ + pip install -r requirements.txt; \ + else \ + echo "No requirements.txt found"; \ + fi + +EXPOSE 8088 + +CMD ["python", "main.py"] diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/README.md new file mode 100644 index 0000000000..98254f212c --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/README.md @@ -0,0 +1,126 @@ +# What this sample demonstrates + +An [Agent Framework](https://github.com/microsoft/agent-framework) agent with persistent semantic memory backed by an **Azure AI Foundry Memory Store**, hosted using the **Responses protocol**. The agent remembers facts the user has shared (e.g., dietary preferences, name) across sessions by retrieving and updating memories around every model invocation via `FoundryMemoryProvider`. + +## How It Works + +### Model Integration + +The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. `allow_preview=True` is passed so the same `AIProjectClient` can also call the preview `beta.memory_stores` API. + +### Memory via Foundry Memory Store + +`FoundryMemoryProvider` is wired into the agent as a context provider. Around each model invocation it: + +1. **Retrieves user-profile memories** for the configured `scope` (e.g., user id) on the first turn of a session. +2. **Searches for contextual memories** matching the current user message and injects them into the model context. +3. **Updates the store** with new facts inferred from the conversation. + +Crucially, the provider is constructed with `project_client=client.project_client` — i.e. it reuses the `AIProjectClient` that `FoundryChatClient` already created, instead of allocating a second one. This keeps a single authentication context and connection pool for both chat and memory operations. + +See [main.py](main.py) for the full implementation. + +### Agent Hosting + +The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +## Prerequisites + +- An Azure AI Foundry project with: + - A deployed chat model (e.g., `gpt-4.1-mini`) + - A deployed embedding model (e.g., `text-embedding-3-small`) — used by the memory store itself, not by the agent at runtime +- Azure CLI logged in (`az login`) + +### Required RBAC + +Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both provisioning the memory store with `provision_memory_store.py` and reading/writing memories from `main.py`. + +## Provisioning the memory store (one time) + +[`provision_memory_store.py`](provision_memory_store.py) creates a Foundry Memory Store with the user-profile capability enabled (and chat-summary disabled) using `AIProjectClient.beta.memory_stores.create`. It is safe to re-run: if a store with the same name already exists, the script leaves it alone. + +From this directory, with the venv activated and `az login` done: + +```bash +export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4.1-mini" +export AZURE_OPENAI_EMBEDDING_MODEL="text-embedding-3-small" +export FOUNDRY_MEMORY_STORE_NAME="agent_framework_memory" +python provision_memory_store.py +``` + +Or in PowerShell: + +```powershell +$env:FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4.1-mini" +$env:AZURE_OPENAI_EMBEDDING_MODEL="text-embedding-3-small" +$env:FOUNDRY_MEMORY_STORE_NAME="agent_framework_memory" +python provision_memory_store.py +``` + +Expected output (first run): + +```text +Creating memory store 'agent_framework_memory'... +Created memory store 'agent_framework_memory' (id=memstore_...). +``` + +> To delete the store manually, call `project.beta.memory_stores.delete("")` on an `AIProjectClient` constructed with `allow_preview=True`. + +## Running the Agent Host + +Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host. + +In addition to the standard environment variables, this sample requires: + +```bash +export FOUNDRY_MEMORY_STORE_NAME="agent_framework_memory" +# Optional — defaults to "user_123" in main.py if unset. +export FOUNDRY_MEMORY_SCOPE="user_123" +``` + +Or in PowerShell: + +```powershell +$env:FOUNDRY_MEMORY_STORE_NAME="agent_framework_memory" +$env:FOUNDRY_MEMORY_SCOPE="user_123" +``` + +You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example). + +## Interacting with the agent + +> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. + +Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. The first request seeds a memory; subsequent requests (especially in new sessions) should be able to recall it because memories are scoped to `FOUNDRY_MEMORY_SCOPE`, not to a particular session. + +```bash +# 1. Tell the agent something to remember. +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" \ + -d '{"input": "I prefer dark roast coffee and I am allergic to nuts."}' + +# Wait a few seconds for the memory to be stored, then start a fresh conversation: +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" \ + -d '{"input": "Can you recommend a coffee and a snack for me?"}' + +curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" \ + -d '{"input": "What do you remember about my preferences?"}' +``` + +The agent's answers to the follow-up turns should reflect the preferences shared in the first turn, even though `default_options={"store": False}` is set (so the Responses service is not persisting conversation history) — the recall is coming exclusively from the Foundry Memory Store. + +## Deploying the Agent to Foundry + +To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory. + +When deploying, make sure `FOUNDRY_MEMORY_STORE_NAME` and `FOUNDRY_MEMORY_SCOPE` are set in your `azd` environment so they get injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml): + +```bash +azd env set FOUNDRY_MEMORY_STORE_NAME "agent_framework_memory" +azd env set FOUNDRY_MEMORY_SCOPE "user_123" +``` + +If these are not set, running `azd ai agent init -m ` will prompt you to enter them interactively. + +The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to read and write memories at runtime. Make sure you have run `provision_memory_store.py` against the same Foundry project before deploying — otherwise the agent will fail on the first turn when it tries to read from a non-existent store. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/agent.manifest.yaml new file mode 100644 index 0000000000..97333dd70d --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/agent.manifest.yaml @@ -0,0 +1,38 @@ +name: agent-framework-agent-foundry-memory-responses +description: > + An Agent Framework agent with persistent semantic memory backed by an + Azure AI Foundry Memory Store. Uses FoundryMemoryProvider to retrieve and + store memories around each model invocation, allowing the agent to remember + facts about a user across sessions. +metadata: + tags: + - Agent Framework + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Foundry Memory +template: + name: agent-framework-agent-foundry-memory-responses + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + - name: FOUNDRY_MEMORY_STORE_NAME + value: "{{FOUNDRY_MEMORY_STORE_NAME}}" + - name: FOUNDRY_MEMORY_SCOPE + value: "{{FOUNDRY_MEMORY_SCOPE}}" +parameters: + properties: + - name: FOUNDRY_MEMORY_STORE_NAME + secret: false + description: The name of the pre-provisioned Foundry Memory Store the agent will use (e.g., agent_framework_memory) + - name: FOUNDRY_MEMORY_SCOPE + secret: false + description: Scope (e.g., user id) used to isolate memories within the store (e.g., user_123) +resources: + - kind: model + id: gpt-4.1-mini + name: AZURE_AI_MODEL_DEPLOYMENT_NAME diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/agent.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/agent.yaml new file mode 100644 index 0000000000..47960c2c07 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/agent.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: agent-framework-agent-foundry-memory-responses +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: "0.5Gi" +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: FOUNDRY_MEMORY_STORE_NAME + value: ${FOUNDRY_MEMORY_STORE_NAME} + - name: FOUNDRY_MEMORY_SCOPE + value: ${FOUNDRY_MEMORY_SCOPE} diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/main.py new file mode 100644 index 0000000000..1053611caf --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/main.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Foundry Memory hosted agent sample. + +This agent uses :class:`FoundryMemoryProvider` to give an otherwise stateless +hosted agent persistent, semantic memory backed by an Azure AI Foundry +Memory Store. The store itself is provisioned once via +``provision_memory_store.py`` and its name is passed in through the +``FOUNDRY_MEMORY_STORE_NAME`` environment variable. + +Unlike the standalone ``azure_ai_foundry_memory.py`` sample, here we construct +the :class:`FoundryChatClient` first and then reuse its underlying +``AIProjectClient`` for the memory provider, so both share a single client +instance and authentication context. +""" + +import asyncio +import os + +from agent_framework import Agent +from agent_framework.foundry import FoundryChatClient, FoundryMemoryProvider +from agent_framework_foundry_hosting import ResponsesHostServer +from azure.identity.aio import DefaultAzureCredential +from dotenv import load_dotenv + +load_dotenv() + + +async def main() -> None: + # The chat client owns the AIProjectClient. ``allow_preview=True`` is required + # so the same client can call the preview ``beta.memory_stores`` API used by + # FoundryMemoryProvider. + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=DefaultAzureCredential(), + allow_preview=True, + ) + + # Reuse the project_client that FoundryChatClient just created, instead of + # constructing a second one for the memory provider. + memory_provider = FoundryMemoryProvider( + project_client=client.project_client, + memory_store_name=os.environ["FOUNDRY_MEMORY_STORE_NAME"], + # Scope namespaces memories (e.g., per end-user). When unset, the + # provider falls back to the session id, which limits memories to a + # single session. + scope=os.environ.get("FOUNDRY_MEMORY_SCOPE", "user_123"), + # In production, leave update_delay at its default to batch updates and + # reduce cost. We use 0 here so memories are written immediately, which + # makes the sample easier to demo. + update_delay=0, + ) + + agent = Agent( + client=client, + instructions=( + "You are a helpful assistant that remembers facts the user has shared " + "across conversations. Relevant memories from previous interactions are " + "automatically provided to you in the system context. Use them when " + "answering, and acknowledge when you are relying on remembered facts." + ), + context_providers=[memory_provider], + # History will be managed by the hosting infrastructure, thus there + # is no need to store history by the service. Learn more at: + # https://developers.openai.com/api/reference/resources/responses/methods/create + default_options={"store": False}, + ) + server = ResponsesHostServer(agent) + await server.run_async() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/provision_memory_store.py b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/provision_memory_store.py new file mode 100644 index 0000000000..0df3c3ecda --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/provision_memory_store.py @@ -0,0 +1,78 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Provision the Azure AI Foundry Memory Store used by this sample. + +Creates the memory store named by ``FOUNDRY_MEMORY_STORE_NAME`` if it does not +already exist. The store is configured with the user-profile capability so the +agent can remember stable facts about a user across sessions; chat-summary is +disabled to keep the demo focused on durable preferences. Safe to re-run: if a +store with the same name already exists, the script leaves it alone. + +Usage (from this directory, with the venv activated and ``az login`` done): + + python provision_memory_store.py + +Required env vars (also read from a local ``.env`` file if present): + + FOUNDRY_PROJECT_ENDPOINT e.g. https://.services.ai.azure.com/api/projects/ + AZURE_AI_MODEL_DEPLOYMENT_NAME Chat model deployment used by the memory store + AZURE_OPENAI_EMBEDDING_MODEL Embedding model deployment used by the memory store + FOUNDRY_MEMORY_STORE_NAME Name of the memory store to create + +Your identity needs ``Azure AI User`` on the Foundry project scope. +""" + +import asyncio +import os + +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + MemoryStoreDefaultDefinition, + MemoryStoreDefaultOptions, +) +from azure.core.exceptions import ResourceNotFoundError +from azure.identity.aio import DefaultAzureCredential +from dotenv import load_dotenv + + +async def main() -> None: + load_dotenv() + + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + memory_store_name = os.environ["FOUNDRY_MEMORY_STORE_NAME"] + chat_model = os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"] + embedding_model = os.environ["AZURE_OPENAI_EMBEDDING_MODEL"] + + async with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project, + ): + try: + existing = await project.beta.memory_stores.get(name=memory_store_name) + print(f"Memory store '{existing.name}' already exists (id={existing.id}); leaving as-is.") + return + except ResourceNotFoundError: + pass + + print(f"Creating memory store '{memory_store_name}'...") + definition = MemoryStoreDefaultDefinition( + chat_model=chat_model, + embedding_model=embedding_model, + options=MemoryStoreDefaultOptions( + chat_summary_enabled=False, + user_profile_enabled=True, + user_profile_details=( + "Avoid irrelevant or sensitive data, such as age, financials, precise location, and credentials" + ), + ), + ) + created = await project.beta.memory_stores.create( + name=memory_store_name, + description="Memory store for the Agent Framework foundry-hosted memory sample", + definition=definition, + ) + print(f"Created memory store '{created.name}' (id={created.id}).") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/requirements.txt b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/requirements.txt new file mode 100644 index 0000000000..8bd2801eff --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/10_foundry_memory/requirements.txt @@ -0,0 +1,3 @@ +agent-framework +agent-framework-foundry-hosting +azure-ai-projects