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 3320e5675 [docs][python] Fix setup, Quickstart and scalar Table output
(#1316)
3320e5675 is described below
commit 3320e56752bace0574fa005e342660e0d557ab96
Author: Yu Tang <[email protected]>
AuthorDate: Thu Oct 22 14:43:25 2026 +0800
[docs][python] Fix setup, Quickstart and scalar Table output (#1316)
Generated-by: Codex CLI 0.163.0-alpha.11.1 (GPT-6)
---
.../docs/development/integrate_with_flink.md | 5 +++
.../quickstart/create-agent/multiple-agents.md | 3 ++
.../quickstart/create-agent/single-agent.md | 1 +
.../docs/get-started/quickstart/yaml_agent.md | 2 +-
docs/content/docs/get-started/setup.md | 9 ++++--
docs/layouts/partials/docs/menu-filetree.html | 13 ++++++--
docs/layouts/partials/docs/title.html | 10 +-----
examples/src/main/resources/input_data.txt | 3 +-
.../flink_integration_agent.py | 9 ++++++
.../flink_intergration_test.py | 37 ++++++++++++++++++++++
.../quickstart/workflow_single_agent_example.py | 1 +
python/flink_agents/runtime/output_type_utils.py | 21 +++++++++---
.../runtime/tests/test_output_type_utils.py | 31 ++++++++++++++++--
.../tests/test_remote_execution_environment.py | 26 +++++++++++++++
14 files changed, 148 insertions(+), 23 deletions(-)
diff --git a/docs/content/docs/development/integrate_with_flink.md
b/docs/content/docs/development/integrate_with_flink.md
index 9ec8ea98b..1bea16897 100644
--- a/docs/content/docs/development/integrate_with_flink.md
+++ b/docs/content/docs/development/integrate_with_flink.md
@@ -157,6 +157,11 @@ AgentsExecutionEnvironment agentsEnv =
Integrate the agent with input `Table`, and return the output `Table` can be
consumed by downstream.
+The input `Table` must be **insert-only**: it may append rows, but must not
update or
+delete earlier rows. Both `from_table()` and `fromTable()` reject Tables that
produce
+updates or deletes, such as a continuous `GROUP BY` aggregation whose results
change
+as new records arrive.
+
{{< tabs "From/To Table" >}}
{{< tab "Python" >}}
diff --git
a/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
b/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
index 49216fa83..691aab0dd 100644
--- a/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
+++ b/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
@@ -542,3 +542,6 @@ $FLINK_HOME/bin/flink run -c
org.apache.flink.agents.examples.WorkflowMultipleAg
The job appears in the Flink web UI at
[localhost:8081](http://localhost:8081). Suggestions
appear in the TaskManager output log under `$FLINK_HOME/log` once a window
closes, which takes
up to a minute after the first review is analyzed.
+
+This example reads a finite file, so the job may finish before a
processing-time window
+fires. Some or all suggestions may therefore not be produced.
diff --git
a/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
b/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
index bdacd2baa..24af388fa 100644
--- a/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
+++ b/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
@@ -55,6 +55,7 @@ caps how many chat requests run at once.
```python
# Set up the Flink streaming environment and the Agents execution environment.
env = StreamExecutionEnvironment.get_execution_environment()
+env.set_parallelism(1)
agents_env = AgentsExecutionEnvironment.get_execution_environment(env)
# limit async request to avoid overwhelming ollama server
diff --git a/docs/content/docs/get-started/quickstart/yaml_agent.md
b/docs/content/docs/get-started/quickstart/yaml_agent.md
index eb37be532..6f041eb92 100644
--- a/docs/content/docs/get-started/quickstart/yaml_agent.md
+++ b/docs/content/docs/get-started/quickstart/yaml_agent.md
@@ -376,5 +376,5 @@ $FLINK_HOME/bin/flink run -c
org.apache.flink.agents.examples.YamlWorkflowAgentE
{{< /tabs >}}
The job appears in the Flink web UI at
[localhost:8081](http://localhost:8081). After a few
-minutes, one JSON record per input review appears in the TaskManager output
log under
+minutes, one analysis result per input review appears in the TaskManager
output log under
`$FLINK_HOME/log`.
diff --git a/docs/content/docs/get-started/setup.md
b/docs/content/docs/get-started/setup.md
index f9eb97b7e..bf9b82cb1 100644
--- a/docs/content/docs/get-started/setup.md
+++ b/docs/content/docs/get-started/setup.md
@@ -183,8 +183,14 @@ pip install flink-agents apache-flink==${FLINK_VERSION}
```shell
echo "setuptools<82" > /tmp/constraint.txt
-PIP_CONSTRAINT=/tmp/constraint.txt pip install flink-agents
apache-flink==${FLINK_VERSION}
+PIP_CONSTRAINT=/tmp/constraint.txt \
+PIP_BUILD_CONSTRAINT=/tmp/constraint.txt \
+ pip install flink-agents apache-flink==${FLINK_VERSION}
```
+
+Set both variables for compatibility with older pip versions. Since pip 26.2,
+`PIP_CONSTRAINT` no longer affects isolated builds, which require
+[`PIP_BUILD_CONSTRAINT`](https://pip.pypa.io/en/stable/user_guide/#build-constraints).
{{< /hint >}}
#### From Source
@@ -206,7 +212,6 @@ pip install apache-flink==${FLINK_VERSION}
# Build Flink Agents
cd flink-agents
./tools/build.sh
-cd ..
```
On Apple Silicon, that `pip install` can fail while building `apache-beam`. See
diff --git a/docs/layouts/partials/docs/menu-filetree.html
b/docs/layouts/partials/docs/menu-filetree.html
index b583347dd..187b54a6d 100644
--- a/docs/layouts/partials/docs/menu-filetree.html
+++ b/docs/layouts/partials/docs/menu-filetree.html
@@ -48,6 +48,13 @@ under the License.
{{ define "book-page-link" }}{{/* (dict "Page" .Page "CurrentPage"
.CurrentPage) */}}
{{ $current := eq .CurrentPage .Page }}
{{ $ancestor := .Page.IsAncestor .CurrentPage }}
+ {{ $title := partial "docs/title" .Page }}
+ {{ if .Page.Params.icon }}
+ {{ $title = printf "%s %s" .Page.Params.icon $title }}
+ {{ end }}
+ {{ if .Page.Params.bold }}
+ {{ $title = printf `<div
style="font-weight:450;margin-bottom:0.5em">%s</div>` $title }}
+ {{ end }}
{{ if .Page.Params.sectionBreak }}
<br/>
@@ -55,14 +62,14 @@ under the License.
{{ if .Page.Params.bookCollapseSection }}
<input type="checkbox" id="section-{{ md5 .Page }}" class="toggle" {{ if
or $current $ancestor }}checked{{ end }} />
<label for="section-{{ md5 .Page }}" class="flex justify-between">
- {{- partial "docs/title" .Page -}}
+ {{- $title | safeHTML -}}
<span>▾</span>
</label>
{{ else if .Page.Content }}
<a href="{{ .Page.Permalink }}" class="{{ if $current }} active{{ end }}">
- {{- partial "docs/title" .Page -}}
+ {{- $title | safeHTML -}}
</a>
{{ else }}
- <span>{{- partial "docs/title" .Page -}}</span>
+ <span>{{- $title | safeHTML -}}</span>
{{ end }}
{{ end }}
diff --git a/docs/layouts/partials/docs/title.html
b/docs/layouts/partials/docs/title.html
index f9c96daf7..f62d444ca 100644
--- a/docs/layouts/partials/docs/title.html
+++ b/docs/layouts/partials/docs/title.html
@@ -30,13 +30,5 @@ under the License.
{{ $title = .File.BaseFileName | humanize | title }}
{{ end }}
-{{ if .Params.icon }}
- {{ $title = printf "%s %s" .Params.icon $title }}
-{{ end }}
-
-{{ if .Params.bold }}
- {{ $title = printf `<div
style="font-weight:450;margin-bottom:0.5em">%s</div>` $title }}
-{{ end }}
-
-{{ return ($title | safeHTML) }}
+{{ return $title }}
diff --git a/examples/src/main/resources/input_data.txt
b/examples/src/main/resources/input_data.txt
index 0e4fae326..9a2e269bc 100644
--- a/examples/src/main/resources/input_data.txt
+++ b/examples/src/main/resources/input_data.txt
@@ -2485,7 +2485,8 @@
{"id": "B009MA34NY", "review": "love these shoes. use them to workout inside
and to work"}
{"id": "B0014F7B98", "review": "Lightweight, comfortable, supportive. Very
happy I bought these shoes for working out."}
{"id": "B000YFSR5G", "review": "Did not fit well. Was not comfortable.
Switched to \"Fruit of the loom\". Fruit of the loom are somewhat oversized but
fit well and are very comfortable."}
-{"id": "B005AGO4LU", "review": "I wear these everyday to work, the gym,
etc."}{"id": "B0058YEJ5K", "review": "Love the fit"}
+{"id": "B005AGO4LU", "review": "I wear these everyday to work, the gym, etc."}
+{"id": "B0058YEJ5K", "review": "Love the fit"}
{"id": "B0058YEJ5K", "review": "Super comfy!"}
{"id": "B0014F7B98", "review": "I have gotten so many compliments on the blue
color and look of this shoe. It is super comfortable. The grip is wonderful
for my indoor gym cross training classes. I always order a half size larger in
Nike brand shoes and thy fit perfectly as I did with this shoe."}
{"id": "B000YFSR5G", "review": "I only give four stars because the pants have
no pockets."}
diff --git
a/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
b/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
index a90145fdb..c95307850 100644
---
a/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
+++
b/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
@@ -171,6 +171,15 @@ class TableAgent(Agent):
ctx.send_event(OutputEvent(output=content))
+class ScalarOutputAgent(Agent):
+ """Emit a scalar unchanged through the real action/output serialization
path."""
+
+ @action(EventType.InputEvent)
+ @staticmethod
+ def emit(event: Event, ctx: RunnerContext) -> None:
+
ctx.send_event(OutputEvent(output=InputEvent.from_event(event).input["value"]))
+
+
class DataStreamToTableAgent(Agent):
"""Agent used for explaining integrating agents from table to datastream.
diff --git
a/python/flink_agents/e2e_tests/e2e_tests_integration/flink_intergration_test.py
b/python/flink_agents/e2e_tests/e2e_tests_integration/flink_intergration_test.py
index 2bc1887cc..a6593e9eb 100644
---
a/python/flink_agents/e2e_tests/e2e_tests_integration/flink_intergration_test.py
+++
b/python/flink_agents/e2e_tests/e2e_tests_integration/flink_intergration_test.py
@@ -18,8 +18,10 @@
import json
import os
import sysconfig
+from collections import Counter
from pathlib import Path
+import pytest
from pyflink.common import Configuration, Encoder, WatermarkStrategy
from pyflink.common.typeinfo import BasicTypeInfo, ExternalTypeInfo,
RowTypeInfo, Types
from pyflink.datastream import (
@@ -32,6 +34,7 @@ from pyflink.datastream.connectors.file_system import (
StreamingFileSink,
)
from pyflink.table import DataTypes, Schema, StreamTableEnvironment,
TableDescriptor
+from pyflink.table.types import DataType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.e2e_tests.e2e_tests_integration.flink_integration_agent
import (
@@ -39,6 +42,7 @@ from
flink_agents.e2e_tests.e2e_tests_integration.flink_integration_agent import
DataStreamToTableAgent,
ItemData,
MyKeySelector,
+ ScalarOutputAgent,
TableAgent,
)
from flink_agents.e2e_tests.test_utils import check_result
@@ -48,6 +52,39 @@ current_dir = Path(__file__).parent
os.environ["PYTHONPATH"] = sysconfig.get_paths()["purelib"]
[email protected]("table_input", [False, True], ids=["datastream",
"table"])
[email protected](
+ ("values", "data_type"),
+ [
+ ([42, -7, 0, None], DataTypes.INT()),
+ ([3.5, 0.0, None], DataTypes.DOUBLE()),
+ ([True, False, None], DataTypes.BOOLEAN()),
+ (["hello", "", None], DataTypes.STRING()),
+ ],
+ ids=["int", "float", "bool", "string"],
+)
+def test_scalar_output_to_single_column_table(
+ values: list, data_type: DataType, table_input: bool
+) -> None:
+ config = Configuration()
+ config.set_string("restart-strategy.type", "disable")
+ env = StreamExecutionEnvironment.get_execution_environment(config)
+ env.set_runtime_mode(RuntimeExecutionMode.STREAMING)
+ env.set_parallelism(1)
+ t_env = StreamTableEnvironment.create(env)
+ agents_env = AgentsExecutionEnvironment.get_execution_environment(env,
t_env)
+ if table_input:
+ source = t_env.from_elements([(value,) for value in values], ["value"])
+ builder = agents_env.from_table(source, key_selector=lambda _: "key")
+ else:
+ source = env.from_collection([{"value": value} for value in values])
+ builder = agents_env.from_datastream(source, key_selector=lambda _:
"key")
+ schema = Schema.new_builder().column("result", data_type).build()
+ table = builder.apply(ScalarOutputAgent()).to_table(schema=schema)
+ with table.execute().collect() as results:
+ assert Counter(row[0] for row in results) == Counter(values)
+
+
def test_from_datastream_to_datastream(tmp_path: Path) -> None:
config = Configuration()
config.set_string("state.backend.type", "rocksdb")
diff --git
a/python/flink_agents/examples/quickstart/workflow_single_agent_example.py
b/python/flink_agents/examples/quickstart/workflow_single_agent_example.py
index d05b328ef..38ed777c4 100644
--- a/python/flink_agents/examples/quickstart/workflow_single_agent_example.py
+++ b/python/flink_agents/examples/quickstart/workflow_single_agent_example.py
@@ -46,6 +46,7 @@ def main() -> None:
"""
# Set up the Flink streaming environment and the Agents execution
environment.
env = StreamExecutionEnvironment.get_execution_environment()
+ env.set_parallelism(1)
agents_env = AgentsExecutionEnvironment.get_execution_environment(env)
# limit async request to avoid overwhelming ollama server
diff --git a/python/flink_agents/runtime/output_type_utils.py
b/python/flink_agents/runtime/output_type_utils.py
index 0e08ddbe0..58bafb014 100644
--- a/python/flink_agents/runtime/output_type_utils.py
+++ b/python/flink_agents/runtime/output_type_utils.py
@@ -263,20 +263,31 @@ def row_shape(row_type_info: RowTypeInfo) -> tuple:
def to_row(value: Any, shape: tuple) -> Any:
"""Adapt an emitted value to a physical ``Row`` using a :func:`row_shape`.
+ A scalar output supplies the only column of a single-column row. This
+ wrapping applies only to the outer output row, so a null nested row remains
+ null rather than becoming a row containing null.
+
The raw agent output crosses a JSON boundary, so a structured value arrives
as a dict (a Pydantic model / dataclass degraded to its fields) or, for a
``NamedTuple``, as a positional array; a value the agent emitted as a
``Row``
is reconstructed as one. Only fields the shape marks as nested rows are
- converted recursively; every other value -- scalar, map, or list -- is left
- untouched for its own coder, so no column kind is silently reshaped. This
is
- the payload of the Table/Row conversion operator.
+ converted recursively; their scalar, map, and list fields are left
untouched
+ for their own coders. This is the payload of the Table/Row conversion
operator.
"""
+ names, _ = shape
+ if len(names) == 1 and not isinstance(value, Row | dict | list | tuple):
+ return Row(value)
+ return _to_row(value, shape)
+
+
+def _to_row(value: Any, shape: tuple) -> Any:
+ """Convert structured row values without wrapping scalar or null fields."""
if isinstance(value, Row):
return value
names, children = shape
if isinstance(value, dict):
fields = [
- to_row(value.get(name), child) if child is not None else
value.get(name)
+ _to_row(value.get(name), child) if child is not None else
value.get(name)
for name, child in zip(names, children, strict=False)
]
return Row(*fields)
@@ -286,7 +297,7 @@ def to_row(value: Any, shape: tuple) -> Any:
# order. A length mismatch means the sequence is one column's own value
# (for example an ARRAY), which is left untouched for its coder.
fields = [
- to_row(item, child) if child is not None else item
+ _to_row(item, child) if child is not None else item
for item, child in zip(value, children, strict=False)
]
return Row(*fields)
diff --git a/python/flink_agents/runtime/tests/test_output_type_utils.py
b/python/flink_agents/runtime/tests/test_output_type_utils.py
index 67d5d137e..60c493530 100644
--- a/python/flink_agents/runtime/tests/test_output_type_utils.py
+++ b/python/flink_agents/runtime/tests/test_output_type_utils.py
@@ -218,12 +218,39 @@ def test_to_row_passes_through_row():
assert to_row(existing, shape) is existing
-def test_to_row_passes_through_non_dict():
- # A non-mapping value is left untouched for its own coder.
[email protected]("value", [42, -7, 3.5, True, False, "hello", "",
None])
+def test_to_row_wraps_scalar_in_single_column(value):
+ # Exercise the same JSON boundary as an agent's OutputEvent.
+ event = OutputEvent(output=value)
+ degraded = Event.from_json(event.model_dump_json()).attributes["output"]
+ shape = row_shape(RowTypeInfo([Types.STRING()], ["result"]))
+ assert to_row(degraded, shape) == Row(value)
+
+
+def test_to_row_does_not_wrap_scalar_for_multiple_columns():
shape = row_shape(infer_row_type_info(ModelOutput))
assert to_row(42, shape) == 42
[email protected]("nested", [None, {"value": None}, {"value": 42}])
+def test_to_row_preserves_nested_row_and_collection_fields(nested):
+ shape = row_shape(
+ RowTypeInfo(
+ [
+ RowTypeInfo([Types.INT()], ["value"]),
+ Types.MAP(Types.STRING(), Types.INT()),
+ Types.LIST(Types.INT()),
+ ],
+ ["nested", "mapping", "items"],
+ )
+ )
+ mapping, items = {"a": 1}, [1, 2]
+ converted = to_row({"nested": nested, "mapping": mapping, "items": items},
shape)
+ assert converted[0] == (None if nested is None else Row(nested["value"]))
+ assert converted[1] is mapping
+ assert converted[2] is items
+
+
def test_to_row_named_tuple_through_output_event_boundary():
"""A ``NamedTuple`` degrades to a positional array across the JSON
boundary.
diff --git
a/python/flink_agents/runtime/tests/test_remote_execution_environment.py
b/python/flink_agents/runtime/tests/test_remote_execution_environment.py
index 5256fffa7..2f9b6f88a 100644
--- a/python/flink_agents/runtime/tests/test_remote_execution_environment.py
+++ b/python/flink_agents/runtime/tests/test_remote_execution_environment.py
@@ -562,6 +562,32 @@ def test_to_table_schema_only_builds_row_stream():
assert t_env.from_data_stream.call_args.args[1] is schema
[email protected]("use_schema", [False, True])
+def test_scalar_table_view_preserves_scalar_datastream_views(use_schema):
+ schema = (
+ Schema.new_builder()
+ .column("result", DataTypes.INT())
+ .column_by_expression("doubled", "result * 2")
+ .build()
+ )
+ with _applied_builder() as (builder, raw_mock, _, _):
+ builder.to_datastream()
+ builder.to_datastream(Types.INT())
+ if use_schema:
+ builder.to_table(schema=schema)
+ else:
+ builder.to_table(output_type=Types.ROW_NAMED(["result"],
[Types.INT()]))
+ payload = cloudpickle.dumps(42)
+ raw_view, scalar_view, row_view = raw_mock.map.call_args_list
+ assert raw_view.args[0](payload) == 42
+ assert scalar_view.args[0](payload) == 42
+ assert row_view.args[0](payload) == Row(42)
+ assert isinstance(row_view.kwargs["output_type"], ExternalTypeInfo)
+ # The worker closure remains picklable; only physical columns consume
values.
+ restored = cloudpickle.loads(cloudpickle.dumps(row_view.args[0]))
+ assert restored(payload) == Row(42)
+
+
def test_to_table_output_type_derives_schema():
with _applied_builder() as (builder, _, t_env, _):
builder.to_table(output_type=ModelOutput)