This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 7dcbe9f4 [doc] Add a structured output user guide (#1252)
7dcbe9f4 is described below
commit 7dcbe9f404704bf93ce8b9a13798ee6a1ac9e485
Author: Weiqing Yang <[email protected]>
AuthorDate: Wed Oct 14 19:23:26 2026 -0700
[doc] Add a structured output user guide (#1252)
Generated-by: GitHub Copilot CLI 1.0.98 (Claude Sonnet 5.5)
---
.../calling-llms/chat-models/providers.md | 6 +-
docs/content/docs/development/calling-llms/chat.md | 4 +-
.../development/calling-llms/structured-output.md | 325 +++++++++++++++++++++
docs/content/docs/development/react_agent.md | 2 +
4 files changed, 334 insertions(+), 3 deletions(-)
diff --git
a/docs/content/docs/development/calling-llms/chat-models/providers.md
b/docs/content/docs/development/calling-llms/chat-models/providers.md
index 525a44bd..488b42a0 100644
--- a/docs/content/docs/development/calling-llms/chat-models/providers.md
+++ b/docs/content/docs/development/calling-llms/chat-models/providers.md
@@ -26,6 +26,8 @@ under the License.
## Built-in Providers
+For which providers and models enforce an `output_schema` natively, see
[Structured Output]({{< ref
"docs/development/calling-llms/structured-output#provider-support" >}}).
+
### Amazon Bedrock
Amazon Bedrock provides access to a wide range of foundation models from
leading AI providers through a unified API. The Flink Agents Bedrock
integration uses the [Converse
API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html),
which provides a consistent interface across all supported models with native
tool calling support. Authentication is handled via SigV4 using the AWS default
credentials chain. No API keys are required.
@@ -337,7 +339,7 @@ The target is `OpenAICompletionsConnection` rather than
`AzureOpenAIChatModelCon
|-----------|------|---------|-------------|
| `connection` | str | Required | Reference to connection method name |
| `model` | str | Required | Name of OpenAI model deployment on Azure |
-| `model_of_azure_deployment` | str | None | The underlying model name (e.g.,
'gpt-4', 'gpt-35-turbo'). Used for token metrics tracking |
+| `model_of_azure_deployment` | str | None | The underlying model name (e.g.,
'gpt-4', 'gpt-35-turbo'). Used for token metrics tracking, and to decide
whether an `output_schema` is applied natively (see [Structured Output]({{< ref
"docs/development/calling-llms/structured-output#provider-support" >}})) |
| `prompt` | Prompt \| str | None | Prompt template or reference to prompt
resource |
| `tools` | List[str] | None | List of tool names available to the model |
| `temperature` | float | None | Sampling temperature (0.0 to 2.0). Not
supported by reasoning models |
@@ -353,7 +355,7 @@ The target is `OpenAICompletionsConnection` rather than
`AzureOpenAIChatModelCon
|-----------|------|---------|-------------|
| `connection` | String | Required | Reference to connection method name |
| `model` | String | Required | Azure deployment name (not the underlying
OpenAI model name) |
-| `model_of_azure_deployment` | String | None | The underlying model name
(e.g., 'gpt-4', 'gpt-4o'). Used solely for token metrics tracking |
+| `model_of_azure_deployment` | String | None | The underlying model name
(e.g., 'gpt-4', 'gpt-4o'). Used for token metrics tracking, and to decide
whether an `output_schema` is applied natively (see [Structured Output]({{< ref
"docs/development/calling-llms/structured-output#provider-support" >}})) |
| `prompt` | Prompt \| String | None | Prompt template or reference to prompt
resource |
| `tools` | List<String> | None | List of tool names available to the model |
| `temperature` | double | None | Sampling temperature (0.0 to 2.0). Not
supported by reasoning models |
diff --git a/docs/content/docs/development/calling-llms/chat.md
b/docs/content/docs/development/calling-llms/chat.md
index f747c473..05072ea8 100644
--- a/docs/content/docs/development/calling-llms/chat.md
+++ b/docs/content/docs/development/calling-llms/chat.md
@@ -183,7 +183,8 @@ In the request, `ollama_chat_model` (Python) or
`ollamaChatModel` (Java) names t
response: accessing the response of a failed event raises an exception.
If you requested structured output, read the parsed result from
-`event.getStructuredOutput()` (Java) or `event.structured_output` (Python).
+`event.getStructuredOutput()` (Java) or `event.structured_output` (Python). See
+[Structured Output]({{< ref "docs/development/calling-llms/structured-output"
>}}) for how to declare a schema and which providers enforce it natively.
Applications that need to recover from a failed chat request should handle the
failed event explicitly. The built-in ReAct agent propagates the failure.
@@ -315,5 +316,6 @@ The built-in `chat_model_action` and `tool_call_action`
coordinate this cycle. F
- Use [Prompts]({{< ref "docs/development/calling-llms/prompts" >}}) to reuse
message templates across requests.
- Add [Tool Use]({{< ref "docs/development/calling-llms/tool_use" >}}) or
[Skills]({{< ref "docs/development/calling-llms/skills" >}}) when the model
needs to perform operations beyond generating an answer.
+- Use [Structured Output]({{< ref
"docs/development/calling-llms/structured-output" >}}) to get a typed object
instead of free text.
- Use [Model Routing]({{< ref "docs/development/calling-llms/model_routing"
>}}) to choose among registered models for each request.
- Consult [Built-in Chat Model Providers]({{< ref
"docs/development/calling-llms/chat-models/providers" >}}) for
provider-specific configuration and media support.
diff --git a/docs/content/docs/development/calling-llms/structured-output.md
b/docs/content/docs/development/calling-llms/structured-output.md
new file mode 100644
index 00000000..bce09d9c
--- /dev/null
+++ b/docs/content/docs/development/calling-llms/structured-output.md
@@ -0,0 +1,325 @@
+---
+title: "Structured Output"
+weight: 8
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+## Overview
+
+Structured output makes a chat model return an object that matches a schema
you define, instead of free text. A [`ChatResponseEvent`]({{< ref
"docs/development/calling-llms/chat#handle-success-and-failure" >}}) carries
the parsed object in `getStructuredOutput()` (Java) or `structured_output`
(Python).
+
+There are two ways to get the model to follow the schema:
+
+- **Native:** the schema is sent in the provider's own structured-output
parameter (for example OpenAI `response_format` or Anthropic `output_config`).
The provider constrains the model's output to the schema.
+- **Prompt:** the model is asked in the prompt text to reply in JSON that
matches the schema, and the framework parses the reply. This works with every
model, but nothing enforces the shape.
+
+You choose a *strategy*, and each provider connection reports whether it can
apply the schema natively. The framework combines the two for every request.
+
+## Declare a Schema
+
+A schema is one of:
+
+| Schema | Python | Java |
+|---|---|---|
+| Typed class | Pydantic `BaseModel` subclass | POJO `Class` |
+| Flink row type | `RowTypeInfo` | `RowTypeInfo` |
+
+Only a typed class can be applied natively. A `RowTypeInfo` always uses the
prompt path.
+
+Pass the schema to a [ReAct Agent]({{< ref "docs/development/react_agent" >}})
with `output_schema`, or put it on a `ChatRequestEvent` when you [send chat
requests yourself]({{< ref "docs/development/calling-llms/chat" >}}).
+
+On a `ChatRequestEvent`:
+
+- **Python:** wrap the schema in `OutputSchema`, as in the example below.
+- **Java:** pass a POJO class directly. Wrap a `RowTypeInfo` with `new
OutputSchema(rowTypeInfo)`. Only the ReAct Agent wraps it for you. A bare
`RowTypeInfo` on the event fails with `Unsupported output schema`.
+
+{{< tabs "Declare a Schema" >}}
+
+{{< tab "Python" >}}
+```python
+class Review(BaseModel):
+ summary: str
+ sentiment: Sentiment # an Enum, see "Enum Fields" below
+
+# ReAct Agent
+agent = ReActAgent(chat_model=chat_model_descriptor, output_schema=Review)
+
+# Custom action
+ctx.send_event(
+ ChatRequestEvent(
+ model="my_chat_model",
+ messages=[
+ ChatMessage.user(
+ "Review the text below. Reply in JSON with the fields"
+ f" summary and sentiment.\n\n{text}"
+ )
+ ],
+ output_schema=OutputSchema(output_schema=Review),
+ )
+)
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+public class Review {
+ public String summary;
+ public Sentiment sentiment; // an enum, see "Enum Fields" below
+}
+
+// ReAct Agent
+ReActAgent agent = new ReActAgent(chatModelDescriptor, prompt, Review.class);
+
+// Custom action
+ctx.sendEvent(
+ new ChatRequestEvent(
+ "myChatModel",
+ List.of(
+ ChatMessage.user(
+ "Review the text below. Reply in JSON with the
fields"
+ + " summary and sentiment.\n\n"
+ + text)),
+ Review.class));
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+## Read the Result
+
+A ReAct Agent emits the parsed object as its output. In a custom action, read
it from the successful `ChatResponseEvent`. For a typed class, Java returns an
instance of the POJO. Python returns the model instance, or its fields as a
`dict` once the event has been serialized, so validate it back into the model.
For a `RowTypeInfo`, the result is a `Row`.
+
+{{< tabs "Read the Result" >}}
+
+{{< tab "Python" >}}
+```python
+@action(ChatResponseEvent.EVENT_TYPE)
+@staticmethod
+def process_response(event: Event, ctx: RunnerContext) -> None:
+ response = cast("ChatResponseEvent", event)
+ if response.is_failed:
+ raise RuntimeError(response.error)
+ review = Review.model_validate(response.structured_output)
+ ctx.send_event(OutputEvent(output=review.summary))
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+@Action(EventType.ChatResponseEvent)
+public static void processResponse(Event event, RunnerContext ctx) throws
Exception {
+ ChatResponseEvent response = ChatResponseEvent.fromEvent(event);
+ if (response.isFailed()) {
+ throw new IllegalStateException(response.getError());
+ }
+ Review review = (Review) response.getStructuredOutput();
+ ctx.sendEvent(new OutputEvent(review.summary));
+}
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+## Choose a Strategy
+
+Set `structured_output_strategy` on the chat model **setup** (not the
connection). The value is case-insensitive.
+
+| Strategy | Behavior |
+|---|---|
+| `AUTO` (default) | Native when the connection recommends native output for
the model (see [Provider Support](#provider-support)). Otherwise the prompt
path, silently. This includes a request that cannot carry the schema. |
+| `NATIVE` | Native whenever the request can carry the schema, even for a
model that is not on the recommended list. If it cannot (for example a
`RowTypeInfo` schema, or a blocker listed in [Provider
Support](#provider-support)), it raises `IllegalArgumentException` (Java) or
`ValueError` (Python) before any model call. |
+| `PROMPT` | Never native. |
+
+{{< tabs "Choose a Strategy" >}}
+
+{{< tab "Python" >}}
+```python
+ResourceDescriptor(
+ clazz=ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP,
+ connection="openai_connection",
+ model="gpt-4o-mini",
+ structured_output_strategy="native",
+)
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP)
+ .addInitialArgument("connection", "openaiConnection")
+ .addInitialArgument("model", "gpt-4o-mini")
+ .addInitialArgument("structured_output_strategy", "native")
+ .build();
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+{{< hint info >}}
+`AUTO` falling back is silent. If a request you expect to be native is not,
check the model name against [Provider Support](#provider-support), or set
`NATIVE` to get an error that names the reason.
+{{< /hint >}}
+
+## How Native Output Behaves
+
+The events your actions send and handle are the same on both paths. These
points differ:
+
+```text
+Prompt path: ChatRequestEvent → model call (tool loop)
+ → parse JSON answer → ChatResponseEvent
+
+Native path: ChatRequestEvent → model call (tool loop, no schema)
+ → one conversion call (schema, no tools)
+ → parse answer → ChatResponseEvent
+```
+
+- **The schema applies to the final answer only.** Tool-calling turns run
without it, so tools work as usual.
+- **Native output adds one conversion call after the tool loop.** The model is
asked to convert its answer into the schema's format without adding
information. This adds latency and cost to each request. The conversion
response is the `response` on the `ChatResponseEvent`.
+- **The prompt path has no conversion call.** The framework does not add the
schema to your messages. The ReAct Agent adds its own instruction. In a custom
action, ask for JSON that matches the schema in your own prompt. Do this even
under `AUTO`, because the request falls back to the prompt path whenever native
output does not apply.
+- **The ReAct Agent adjusts its prompt to the path.** On the native path it
only asks the model to include the information for every required field,
because the conversion call formats it. If it cannot tell whether the call will
be native, it keeps the JSON instruction.
+- **A reply that cannot be parsed fails the request.** You get a failed
`ChatResponseEvent` after the configured retries. See [Failures and
Recovery]({{< ref "docs/development/calling-llms/chat#failures-and-recovery"
>}}).
+
+## Provider Support
+
+"Recommended" is what `AUTO` needs to go native. Models outside it still go
native under `NATIVE`, unless the last column lists a blocker. A blocked
request uses the prompt path under `AUTO` and `PROMPT`, and fails under
`NATIVE`.
+
+| Provider | Languages | Recommended models (`AUTO` goes native) | Blocked
(besides a non-typed-class schema) |
+|---|---|---|---|
+| OpenAI | Java, Python | `gpt-4o-mini`, `gpt-4o-search-preview`, `gpt-4.1`,
`gpt-5` and `o3`, `o4-mini` families (names that start with these), plus
`gpt-4o`, `gpt-4o-2024-08-06`, `gpt-4o-2024-11-20`, `o1`, `o1-2024-12-17`.
Audio, realtime, TTS and transcribe variants are excluded. | |
+| OpenAI Responses | Java | none | Always. Use the prompt path. |
+| Azure OpenAI | Java, Python | The model named by `model_of_azure_deployment`
is one of `gpt-5.1`, `gpt-5.1-chat`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `o3`,
`o3-mini`, `o4-mini`, `o1`, `gpt-4o-mini`, `gpt-4.1`, `gpt-4.1-mini`,
`gpt-4.1-nano`. | `api_version` older than `2024-08-01`, or not a date |
+| vLLM | Java, Python | Any model | Same as OpenAI |
+| Anthropic | Java, Python | `claude-opus-4-6`, `-4-7`, `-4-8`, `-5`;
`claude-sonnet-4-6`, `-5`; `claude-fable-5`; `claude-mythos-5`;
`claude-mythos-preview`; and the `claude-opus-4-5`, `claude-sonnet-4-5`,
`claude-haiku-4-5` aliases and their dated versions | You pass `output_config`
in `additional_kwargs` |
+| Amazon Bedrock | Java | A fixed list of Converse models (a regional or
global inference-profile prefix is ignored). ARNs are never recommended. | |
+| Gemini | Java | `gemini-*` text models. Image, TTS, audio, live, transcribe,
embedding and omni variants are excluded. | The request binds tools |
+| Ollama | Java, Python | Any model | |
+| watsonx | Java, Python | Any model | |
+| DashScope | Python | `qwen3.7-max`, `qwen3.7-max-preview`,
`qwen3.7-max-2026-05-17`, `qwen3.7-max-2026-05-20` | |
+
+Notes:
+
+- **Azure OpenAI** matches `model_of_azure_deployment`, not the deployment
name, because a deployment name can be anything. If you do not set it, `AUTO`
stays on the prompt path (`NATIVE` still goes native). Set it to get `AUTO`
native on Azure, as in the example below.
+- **Gemini** blocks native output when the request binds tools. The conversion
call is tool-free, so this only affects direct connection calls. A newer model
that does not support the schema field fails with `400 INVALID_ARGUMENT`.
Constraints such as `pattern` and `minLength` are ignored by the server.
+- **Ollama** needs a server at v0.5.0 or later. Ollama Cloud and the MLX
runner accept the schema but do not enforce it.
+- **Bedrock** rejects recursive schemas, and map values are untyped.
+- **A native parameter you set yourself conflicts.** Do not put
`response_format` (OpenAI-compatible providers) or `output_config` (Anthropic)
in `additional_kwargs` or model parameters when you use an output schema. Azure
OpenAI, watsonx and DashScope raise an error when the schema would overwrite
your value. Anthropic treats the request as blocked.
+- **Cross-language setups never go native.** A chat model from the other
language, reached through a [cross-language resource]({{< ref
"docs/development/calling-llms/chat-models#using-cross-language-providers"
>}}), cannot carry a schema across the bridge. `AUTO` and `PROMPT` use the
prompt path, and `NATIVE` raises. To use a provider natively, declare the agent
in that provider's language.
+
+For example, this Azure OpenAI setup goes native under `AUTO`, because the
deployment's model is `gpt-4o-mini`. Without `model_of_azure_deployment`, the
same setup would use the prompt path.
+
+{{< tabs "Azure OpenAI Example" >}}
+
+{{< tab "Python" >}}
+```python
+@chat_model_setup
+@staticmethod
+def azure_openai_chat_model() -> ResourceDescriptor:
+ return ResourceDescriptor(
+ clazz=ResourceName.ChatModel.AZURE_OPENAI_SETUP,
+ connection="azure_openai_connection",
+ model="my-deployment", # Your Azure deployment name
+ model_of_azure_deployment="gpt-4o-mini", # The model behind it
+ )
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+@ChatModelSetup
+public static ResourceDescriptor azureOpenAIChatModel() {
+ return
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.AZURE_OPENAI_SETUP)
+ .addInitialArgument("connection", "azureOpenAIConnection")
+ .addInitialArgument("model", "my-deployment") // Your Azure
deployment name
+ .addInitialArgument("model_of_azure_deployment", "gpt-4o-mini") //
The model behind it
+ .build();
+}
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+The connection's `api_version` must be `2024-08-01` or later, for example
`2024-10-21`.
+
+## Schema Rules
+
+### Enum Fields
+
+List the values the model should return, in the form the model reads back:
+
+- **Python:** a Pydantic `Enum` lists its values. A string enum gives `"type":
"string"` and a numeric enum gives `"type": "integer"`.
+- **Java:** the schema lists the values Jackson deserializes. Put
`@JsonProperty` on **every** constant, or use a `@JsonValue` method that
returns a `String`.
+
+{{< tabs "Enum Fields" >}}
+
+{{< tab "Python" >}}
+```python
+class Sentiment(str, Enum):
+ POSITIVE = "positive"
+ NEGATIVE = "negative"
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+public enum Sentiment {
+ @JsonProperty("positive") POSITIVE,
+ @JsonProperty("negative") NEGATIVE
+}
+
+// or
+public enum Sentiment {
+ POSITIVE("positive"),
+ NEGATIVE("negative");
+
+ private final String label;
+
+ Sentiment(String label) {
+ this.label = label;
+ }
+
+ @JsonValue
+ public String label() {
+ return label;
+ }
+}
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+Two Java cases do not work as you might expect. They behave the same on every
Java provider:
+
+- `@JsonProperty` on only **some** constants lists the Java names for **all**
of them (`["POSITIVE","NEGATIVE"]`). The model may reply with a Java name where
you annotated a different value, and parsing fails. Annotate every constant or
none.
+- A `@JsonValue` that returns a number produces
`{"type":"string","enum":[1,-1]}`, which contradicts itself. Use a string value
instead.
+
+### Java POJOs
+
+- **Descriptions and required fields.** `@JsonPropertyDescription` and
`@JsonClassDescription` are sent in the native schema, and every field is
required (an `Optional` field is required but nullable on OpenAI, Azure OpenAI
and Anthropic). The ReAct Agent's *prompt text* is rendered by a different
generator, which has no descriptions, and lists a field as required only when
it has `@JsonProperty(required = true)`. The prompt path therefore describes
the schema less fully than the native path.
+- **`Map` fields.** On OpenAI, Azure OpenAI, Anthropic and vLLM, a
`Map<String, V>` field becomes an empty object schema that allows no keys, so
the model can only return `{}`. Avoid `Map` fields with these providers, or use
a nested class. Gemini, Ollama and watsonx type the map values.
+- **Recursive types.** The ReAct Agent rejects a self-referential POJO at
construction time. Bedrock also rejects a recursive schema.
+
+### Python Models
+
+The schema comes from Pydantic's `model_json_schema()`, so field descriptions
and defaults are included. A model that Pydantic cannot express raises a
`TypeError` naming the schema when it is rendered.
+
+## Next Steps
+
+- See [Built-in Chat Model Providers]({{< ref
"docs/development/calling-llms/chat-models/providers" >}}) for how to declare
each connection and setup.
+- Use the [ReAct Agent]({{< ref "docs/development/react_agent" >}}) to get
structured output from an agent with tools.
diff --git a/docs/content/docs/development/react_agent.md
b/docs/content/docs/development/react_agent.md
index 430ffc66..19dc49bc 100644
--- a/docs/content/docs/development/react_agent.md
+++ b/docs/content/docs/development/react_agent.md
@@ -257,6 +257,8 @@ User can set output schema to configure the ReAct Agent
output type. If output s
The output schema should be a `BaseModel` subclass (Python) / a Pojo class
(Java), or a `RowTypeInfo` (both).
+When the chat model's provider supports it, the schema is enforced natively by
the provider rather than only described in the prompt. See [Structured
Output]({{< ref "docs/development/calling-llms/structured-output" >}}) for the
strategies, supported providers and schema rules.
+
{{< tabs "Output Schema" >}}
{{< tab "Python" >}}