Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/python-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,53 @@ jobs:
path: ./python/pytest.xml
if-no-files-found: ignore

# Foundry Hosting integration tests
python-tests-foundry-hosting:
name: Python Integration Tests - Foundry Hosting
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest (Foundry Hosting integration)
timeout-minutes: 15
run: >
uv run pytest --import-mode=importlib
packages/foundry_hosting/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
if-no-files-found: ignore

# Azure Cosmos integration tests
python-tests-cosmos:
name: Python Integration Tests - Cosmos
Expand Down Expand Up @@ -402,6 +449,7 @@ jobs:
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
]
runs-on: ubuntu-latest
Expand Down Expand Up @@ -465,6 +513,7 @@ jobs:
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos
]
steps:
Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/python-merge-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
miscChanged: ${{ steps.filter.outputs.misc }}
functionsChanged: ${{ steps.filter.outputs.functions }}
foundryChanged: ${{ steps.filter.outputs.foundry }}
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -80,6 +81,8 @@ jobs:
- 'python/packages/foundry/**'
- 'python/samples/**/providers/foundry/**'
- 'python/samples/02-agents/embeddings/foundry_embeddings.py'
foundry_hosting:
- 'python/packages/foundry_hosting/**'
cosmos:
- 'python/packages/azure-cosmos/**'
# run only if 'python' files were changed
Expand Down Expand Up @@ -488,6 +491,67 @@ jobs:
path: ./python/pytest.xml
if-no-files-found: ignore

# Foundry Hosting integration tests
python-tests-foundry-hosting:
name: Python Tests - Foundry Hosting Integration
needs: paths-filter
if: >
github.event_name != 'pull_request' &&
needs.paths-filter.outputs.pythonChanges == 'true' &&
(github.event_name != 'merge_group' ||
needs.paths-filter.outputs.foundryHostingChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest (Foundry Hosting integration)
timeout-minutes: 15
run: >
uv run pytest --import-mode=importlib
packages/foundry_hosting/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Foundry Hosting integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
if-no-files-found: ignore

# TODO: Add python-tests-lab

# Azure Cosmos integration tests
Expand Down Expand Up @@ -569,6 +633,7 @@ jobs:
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
]
runs-on: ubuntu-latest
Expand Down Expand Up @@ -629,6 +694,7 @@ jobs:
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
]
steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import asyncio
import base64
import json
import logging
import os
Expand Down Expand Up @@ -1075,6 +1076,31 @@ def _convert_output_message_content(content: OutputMessageContent) -> Content:
raise ValueError(f"Unsupported OutputMessageContent type: {content.type}")


def _convert_file_data(data_uri: str, filename: str | None = None) -> Content:
"""Convert a file_data data URI to a Content object.

For text/* MIME types, decodes the base64 content and returns it as text.
For other types, returns a URI-based Content with the filename preserved.
"""
# Parse data URI: data:<media_type>;base64,<data>
if data_uri.startswith("data:") and ";base64," in data_uri:
header, encoded = data_uri.split(";base64,", 1)
media_type = header[len("data:") :]
if media_type.startswith("text/"):
try:
decoded_text = base64.b64decode(encoded).decode("utf-8")
except (ValueError, UnicodeDecodeError):
logger.warning(
"Failed to decode text/* file_data as UTF-8, falling through to URI passthrough.",
exc_info=True,
)
else:
prefix = f"[File: {filename}]\n" if filename else ""
return Content.from_text(f"{prefix}{decoded_text}")
additional_properties = {"filename": filename} if filename else None
return Content.from_uri(data_uri, additional_properties=additional_properties)


def _convert_message_content(content: MessageContent) -> Content:
"""Converts a MessageContent to a Content object.

Expand Down Expand Up @@ -1108,7 +1134,9 @@ def _convert_message_content(content: MessageContent) -> Content:
if content.type == "input_image":
image = cast(MessageContentInputImageContent, content)
if image.image_url:
Comment thread
TaoChenOSU marked this conversation as resolved.
return Content.from_uri(image.image_url)
if image.image_url.startswith("data:"):
return Content.from_uri(image.image_url)
return Content.from_uri(image.image_url, media_type="image/*")
if image.file_id:
return Content.from_hosted_file(image.file_id)
if content.type == "input_file":
Expand All @@ -1117,6 +1145,8 @@ def _convert_message_content(content: MessageContent) -> Content:
return Content.from_uri(file.file_url)
if file.file_id:
return Content.from_hosted_file(file.file_id, name=file.filename)
if file.file_data:
return _convert_file_data(file.file_data, file.filename)
if content.type == "computer_screenshot":
screenshot = cast(ComputerScreenshotContent, content)
return Content.from_uri(screenshot.image_url)
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions python/packages/foundry_hosting/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,121 @@ async def test_text_and_file_input_single_turn(self) -> None:
assert messages[0].contents[1].type == "uri"
assert messages[0].contents[1].uri == "https://example.com/doc.pdf"

async def test_text_and_file_data_input_single_turn(self) -> None:
"""Agent receives a message with text and file content via inline file_data."""
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("File received")])])
)
server = _make_server(agent)

resp = await _post_json(
server,
{
"model": "test-model",
"input": [
{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "Summarize this document"},
{
"type": "input_file",
"file_data": "data:application/pdf;base64,JVBERi0xLjQ=",
"filename": "doc.pdf",
},
],
}
],
"stream": False,
},
)

assert resp.status_code == 200
body = resp.json()
assert body["status"] == "completed"

messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert len(messages[0].contents) == 2
assert messages[0].contents[0].type == "text"
assert messages[0].contents[0].text == "Summarize this document"
assert messages[0].contents[1].type == "data"
assert messages[0].contents[1].uri == "data:application/pdf;base64,JVBERi0xLjQ="
Comment thread
TaoChenOSU marked this conversation as resolved.

async def test_text_mime_file_data_decoded(self) -> None:
"""Agent receives a text/* file_data that is base64-decoded to plain text."""
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Got it")])])
)
server = _make_server(agent)

import base64

encoded = base64.b64encode(b"Hello, world!").decode()

resp = await _post_json(
server,
{
"model": "test-model",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_file",
"file_data": f"data:text/plain;base64,{encoded}",
"filename": "greeting.txt",
},
],
}
],
"stream": False,
},
)

assert resp.status_code == 200

messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert messages[0].contents[0].type == "text"
assert messages[0].contents[0].text == "[File: greeting.txt]\nHello, world!"

async def test_text_mime_file_data_invalid_base64_falls_through(self) -> None:
"""Invalid base64 in a text/* file_data falls through to URI passthrough."""
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Got it")])])
)
server = _make_server(agent)

resp = await _post_json(
server,
{
"model": "test-model",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_file",
"file_data": "data:text/plain;base64,!!!invalid!!!",
"filename": "bad.txt",
},
],
}
],
"stream": False,
},
)

assert resp.status_code == 200

messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert messages[0].contents[0].type == "data"
assert messages[0].contents[0].uri == "data:text/plain;base64,!!!invalid!!!"

async def test_mixed_text_and_image_input(self) -> None:
"""Agent receives a single message with both text and image content."""
agent = _make_agent(
Expand Down
Loading
Loading