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
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