Skip to content
114 changes: 114 additions & 0 deletions contributing/samples/models/azure_responses_streaming/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Azure Responses Partial Function-Call Streaming

## Overview

This sample provides a small document-writing agent for testing streamed
function-call arguments. Azure OpenAI Responses is the default provider. The
`create_document` tool has a nested, deliberately detailed Pydantic input
schema, so the model sends enough JSON for partial function-call events to be
visible in the Dev UI and in `run.py`.

The tool writes the generated document to `generated_docs/` inside this sample
directory. It sanitizes the requested filename to keep the example local to
that output directory.

## Setup

Install the OpenAI Responses extra from the repository root:

```bash
uv sync --extra extensions
```

Configure Azure. `AZURE_OPENAI_ENDPOINT` is optional when
`AZURE_RESOURCE_NAME` is set; the sample derives the standard Azure endpoint
from the resource name.

```bash
export AZURE_API_KEY="your-azure-api-key"
export AZURE_RESOURCE_NAME="your-azure-resource-name"
export AZURE_MODEL_DEPLOYMENT="your-model-deployment"
```

For a non-standard endpoint, set it explicitly:

```bash
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com"
```

Do not commit API keys or `.env` files.

## Run With Dev UI

The Dev UI discovers `agent.py` from the sample directory. Enable streaming
in the UI, then ask the agent to create a document.

```bash
uv run --extra extensions adk web contributing/samples/models/azure_responses_streaming
```

Open the URL printed by `adk web`, select the sample agent, and send:

`Create a detailed onboarding guide for backend engineers with four sections, references, and a rollout checklist.`

The UI should show partial function-call content before the final tool call,
followed by the tool result and the generated Markdown path.

## Run `run.py`

`run.py` uses the same `agent.py`, forces `StreamingMode.SSE`, and prints each
text event and function-call delta. Run it from the repository root:

```bash
uv run --extra extensions python contributing/samples/models/azure_responses_streaming/run.py
```

You can provide a custom prompt:

```bash
uv run --extra extensions python contributing/samples/models/azure_responses_streaming/run.py \
Create a security review document with threat model, controls, testing, and remediation sections.
```

Look for lines such as:

```text
[function_call] partial=True ... delta='{"filename": ...'
[function_call] partial=True ... delta='...'
[function_call] partial=False ... args={...}
```

## Sample Inputs

- `Create a technical design brief for a document streaming feature with architecture, API contract, rollout, and testing sections.`

- `Create a detailed onboarding guide for backend engineers with four sections, references, and a rollout checklist.`

- `Create a security review document with a threat model, controls, testing, and remediation sections.`

## Graph

```mermaid
graph TD
DocumentAgent[azure_responses_streaming_agent] -->|calls| CreateDocument[create_document]
```

## How To

- `agent.py` builds the Azure Responses model lazily, so the optional OpenAI
dependency is only imported when the sample starts.
- `DocumentRequest` and `DocumentSection` provide a nested tool schema. Ask for
multiple detailed sections to make raw argument fragments easy to observe.
- `run.py` enables `StreamingMode.SSE` and prints `partial_args` separately
from the final parsed `FunctionCall.args`.
- The Dev UI uses the same `agent.py`; its streaming toggle controls the
request path, while `run.py` is a deterministic terminal harness.

## Related Guides

- [Function tools sample](../../tools/function_tools/README.md) - Register
typed Python functions as agent tools.
- [LLM agent single-turn mode](../../../../docs/guides/agents/llm_agent/single_turn.md) -
Configure a basic LLM agent.
- [Event guide](../../../../docs/guides/events/event/index.md) - Inspect the
events emitted by an agent run.
15 changes: 15 additions & 0 deletions contributing/samples/models/azure_responses_streaming/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from . import agent
172 changes: 172 additions & 0 deletions contributing/samples/models/azure_responses_streaming/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Agent used to exercise streamed function-call arguments."""

from __future__ import annotations

import os
from pathlib import Path

from google.adk import Agent
from google.adk.models.base_llm import BaseLlm
from pydantic import BaseModel
from pydantic import Field


class DocumentSection(BaseModel):
"""A section in the document generated by the tool."""

heading: str
purpose: str
key_points: list[str]
body: str
references: list[str] = Field(default_factory=list)


class DocumentRequest(BaseModel):
"""Structured input deliberately large enough to make streaming visible."""

filename: str
title: str
executive_summary: str
audience: str
language: str
tone: str
keywords: list[str]
sections: list[DocumentSection]
include_table_of_contents: bool = True
footer: str = "Generated by the ADK streaming function-call sample."


def _output_directory() -> Path:
configured_directory = Path(
os.getenv("DOCUMENT_OUTPUT_DIR", "generated_docs")
)
if not configured_directory.is_absolute():
configured_directory = Path(__file__).parent / configured_directory
configured_directory.mkdir(parents=True, exist_ok=True)
return configured_directory


def create_document(document: DocumentRequest) -> dict[str, str | int]:
"""Create a Markdown document from a structured request.

Args:
document: A complete document specification. Include several sections and
detailed key points so the model has a sizable function-call payload to
stream.

Returns:
The path and basic metadata for the generated Markdown file.
"""
filename = Path(document.filename).name
if not filename or filename in {".", ".."}:
filename = "generated_document.md"
if not filename.lower().endswith(".md"):
filename += ".md"

lines = [
f"# {document.title}",
"",
f"**Audience:** {document.audience}",
f"**Language:** {document.language}",
f"**Tone:** {document.tone}",
"",
"## Executive Summary",
"",
document.executive_summary,
"",
]

if document.keywords:
lines.extend(["**Keywords:** " + ", ".join(document.keywords), ""])

if document.include_table_of_contents:
lines.extend(["## Table of Contents", ""])
lines.extend(f"- {section.heading}" for section in document.sections)
lines.append("")

for section in document.sections:
lines.extend([
f"## {section.heading}",
"",
f"**Purpose:** {section.purpose}",
"",
section.body,
"",
"### Key Points",
"",
])
lines.extend(f"- {point}" for point in section.key_points)
if section.references:
lines.extend(["", "### References", ""])
lines.extend(f"- {reference}" for reference in section.references)
lines.append("")

lines.extend(["---", "", document.footer, ""])
output_path = _output_directory() / filename
output_path.write_text("\n".join(lines), encoding="utf-8")
return {
"status": "created",
"path": str(output_path),
"section_count": len(document.sections),
"byte_count": output_path.stat().st_size,
}


def _required_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(
f"Set {name} before starting the sample. See README.md for setup."
)
return value


def _build_model() -> BaseLlm:
"""Build the Azure Responses model used by this sample."""
from google.adk.labs.openai import AzureOpenAIResponsesLlm

endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
if not endpoint:
resource_name = _required_env("AZURE_RESOURCE_NAME")
endpoint = f"https://{resource_name}.openai.azure.com"
return AzureOpenAIResponsesLlm(
model=os.getenv("AZURE_MODEL_DEPLOYMENT", "gpt-4o"),
api_key=_required_env("AZURE_API_KEY"),
azure_endpoint=endpoint,
include_response_metadata=True,
)


root_agent = Agent(
name="azure_responses_streaming_agent",
model=_build_model(),
description=(
"Creates Markdown documents while exposing streamed function-call "
"arguments."
),
instruction=(
"You are a document planning assistant. When the user asks you to "
"create, draft, or write a document, you MUST call create_document. "
"Do not write the full document only in your answer. Build a rich "
"DocumentRequest with a safe Markdown filename, a clear title, an "
"executive summary, audience, language, tone, keywords, and three to "
"six detailed sections. Each section must contain a purpose, body, "
"multiple key points, and references when useful. After the tool "
"returns, tell the user where the file was written."
),
tools=[create_document],
)
94 changes: 94 additions & 0 deletions contributing/samples/models/azure_responses_streaming/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Run the sample in a terminal and print streamed function-call deltas."""

from __future__ import annotations

import argparse
import asyncio

from google.adk.agents.run_config import RunConfig
from google.adk.agents.run_config import StreamingMode
from google.adk.runners import InMemoryRunner
from google.genai import types

try:
from .agent import root_agent
except ImportError:
from agent import root_agent


APP_NAME = "azure_responses_streaming_sample"
USER_ID = "streaming tester"
DEFAULT_PROMPT = (
"Create a technical design brief for a document streaming feature. "
"Target backend engineers, use a precise but approachable tone, and "
"include architecture, API contract, rollout, and testing sections."
)


def _print_event(event: object) -> None:
content = getattr(event, "content", None)
if not content:
return
for part in content.parts or []:
function_call = getattr(part, "function_call", None)
if function_call:
delta = "".join(
partial_arg.string_value or ""
for partial_arg in function_call.partial_args or []
)
print(
"[function_call] "
f"partial={getattr(event, 'partial', None)!r} "
f"id={function_call.id!r} name={function_call.name!r} "
f"delta={delta!r} args={function_call.args!r}"
)
elif part.text:
print(f"[text] partial={getattr(event, 'partial', None)!r} {part.text}")


async def _run(prompt: str) -> None:
runner = InMemoryRunner(agent=root_agent, app_name=APP_NAME)
session = await runner.session_service.create_session(
app_name=APP_NAME,
user_id=USER_ID,
)
content = types.Content(
role="user",
parts=[types.Part.from_text(text=prompt)],
)
async for event in runner.run_async(
user_id=USER_ID,
session_id=session.id,
new_message=content,
run_config=RunConfig(streaming_mode=StreamingMode.SSE),
):
_print_event(event)


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"prompt",
nargs="*",
help="Prompt to send; the sample prompt is used when omitted.",
)
args = parser.parse_args()
asyncio.run(_run(" ".join(args.prompt) or DEFAULT_PROMPT))


if __name__ == "__main__":
main()
Loading