diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/README.md index 5aafdff38b..091a1d0d6b 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/README.md @@ -116,7 +116,7 @@ curl -X POST "$SEARCH_ENDPOINT/indexes/$INDEX_NAME/docs/index?api-version=2024-0 }' ``` -You can also point the sample at any existing index that exposes the four fields above; the provider reads `content`, `sourceName`, and `sourceLink` from the search results. +You can also point the sample at any existing index that exposes a retrievable text field such as `content`. ## Running the Agent Host diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/main.py index ea08b8d78a..73639ef8a4 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/08_azure_search_rag/main.py @@ -7,7 +7,7 @@ from agent_framework import Agent from agent_framework.azure import AzureAISearchContextProvider from agent_framework.foundry import FoundryChatClient from agent_framework_foundry_hosting import ResponsesHostServer -from azure.identity.aio import DefaultAzureCredential +from azure.identity import DefaultAzureCredential from dotenv import load_dotenv # Load environment variables from .env file @@ -37,9 +37,8 @@ async def main(): credential=credential, ) - async with ( - search_provider, - Agent( + async with search_provider: + agent = Agent( client=client, instructions=( "You are a helpful support specialist for Contoso Outdoors. " @@ -51,8 +50,7 @@ async def main(): # 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}, - ) as agent, - ): + ) server = ResponsesHostServer(agent) await server.run_async() diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/.gitignore b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/.gitignore new file mode 100644 index 0000000000..ae8a1dfbe8 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/.gitignore @@ -0,0 +1 @@ +downloaded_skills/ diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/README.md index 3c2bb11227..0831efdb83 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/README.md @@ -130,7 +130,7 @@ When deploying, make sure `SKILL_NAMES` is set in your `azd` environment so it g azd env set SKILL_NAMES "support-style,escalation-policy" ``` -If it is not set, running `azd ai agent init -m ` will prompt you to enter it interactively. +If it is not set, running `azd ai agent init -m ` will prompt you to enter it interactively. The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup. Make sure you have run `provision_skills.py` against the same Foundry project before deploying — otherwise the agent will fail to start with HTTP 404 on the skill download. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/main.py index 539fda1d4d..5cac1493bb 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/main.py @@ -39,6 +39,16 @@ DOWNLOADED_SKILLS_DIR: Final = Path(__file__).parent / "downloaded_skills" logger = logging.getLogger(__name__) +def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None: + """Extract ``zf`` into ``dest_dir``, rejecting entries that escape it (zip-slip guard).""" + dest_root = dest_dir.resolve() + for member in zf.infolist(): + member_path = (dest_root / member.filename).resolve() + if dest_root != member_path and dest_root not in member_path.parents: + raise RuntimeError(f"Refusing to extract unsafe path '{member.filename}' outside of '{dest_root}'.") + zf.extractall(dest_dir) + + async def _bootstrap_skills(endpoint: str, skill_names: list[str], target_dir: Path) -> None: """Download each named skill via ``project.beta.skills`` and unpack it as ``//SKILL.md``.""" if target_dir.exists(): # noqa: ASYNC240 @@ -56,7 +66,7 @@ async def _bootstrap_skills(endpoint: str, skill_names: list[str], target_dir: P skill_dir = target_dir / name skill_dir.mkdir() with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: - zf.extractall(skill_dir) + _safe_extract_zip(zf, skill_dir) if not (skill_dir / "SKILL.md").is_file(): raise RuntimeError(f"Downloaded archive for '{name}' did not contain a SKILL.md at the root.") @@ -77,23 +87,24 @@ async def main() -> None: # instruction-only. skills_provider = SkillsProvider.from_paths(skill_paths=str(DOWNLOADED_SKILLS_DIR)) - client = FoundryChatClient( - project_endpoint=project_endpoint, - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=DefaultAzureCredential(), - ) + async with DefaultAzureCredential() as credential: + client = FoundryChatClient( + project_endpoint=project_endpoint, + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=credential, + ) - agent = Agent( - client=client, - instructions="You are a customer-support assistant for Contoso Outdoors.", - context_providers=[skills_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() + agent = Agent( + client=client, + instructions="You are a customer-support assistant for Contoso Outdoors.", + context_providers=[skills_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__":