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 f7af693f [docs][examples] Add parallel LLM quickstart and examples for 
multi-action fan-out (#830)
f7af693f is described below

commit f7af693f86354cd1d5aa3ac1b71f66c162456c0d
Author: Haocong Wang <[email protected]>
AuthorDate: Tue Jun 16 11:50:39 2026 +0800

    [docs][examples] Add parallel LLM quickstart and examples for multi-action 
fan-out (#830)
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../docs/get-started/quickstart/parallel_llm.md    | 480 +++++++++++++++++++++
 .../examples/ParallelChatRequestExample.java       |  74 ++++
 .../examples/agents/CustomTypesAndResources.java   |  62 +++
 .../agents/examples/agents/ParallelChatAgent.java  | 205 +++++++++
 .../agents/custom_types_and_resources.py           |  14 +
 .../quickstart/agents/parallel_chat_agent.py       | 173 ++++++++
 .../quickstart/parallel_chat_request_example.py    |  74 ++++
 7 files changed, 1082 insertions(+)

diff --git a/docs/content/docs/get-started/quickstart/parallel_llm.md 
b/docs/content/docs/get-started/quickstart/parallel_llm.md
new file mode 100644
index 00000000..5a4b75c0
--- /dev/null
+++ b/docs/content/docs/get-started/quickstart/parallel_llm.md
@@ -0,0 +1,480 @@
+---
+title: 'Parallel LLM Calls'
+weight: 4
+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
+
+Flink Agents supports parallel LLM invocations via a broadcast event model. By 
emitting a single intermediate event from one action and registering multiple 
independent action handlers that each listen on that event type, the 
framework's built-in chat action executes the corresponding LLM calls 
concurrently — no external orchestration is required.
+
+This quickstart introduces an example that demonstrates how to build a 
parallel LLM workflow with Flink Agents:
+
+The **Parallel Sentiment Analysis** agent processes a restaurant review and 
judges sentiment along two dimensions (taste / service) in parallel, then 
aggregates the results into a one-line summary with a final LLM call. The 
end-to-end wall clock time is roughly "slowest single branch + aggregation 
call", rather than the sum of all three calls.
+
+{{< hint info >}}
+**JDK version note (Java only):** On JDK 21+, the framework uses the 
Continuation API to execute concurrent chat actions in parallel. On JDK < 21, 
the framework silently falls back to sequential execution — the result is 
identical, but the LLM calls run one after another. Python uses native 
coroutines and always executes in parallel regardless of the JDK version.
+{{< /hint >}}
+
+## Code Walkthrough
+
+### Prepare Agents Execution Environment
+
+Create the agents execution environment, and register the available chat model 
connection to the environment.
+
+{{< tabs "Prepare Agents Execution Environment" >}}
+
+{{< tab "Python" >}}
+```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)
+
+# Add Ollama chat model connection to be used by the ParallelChatAgent.
+agents_env.add_resource(
+    "ollama_server",
+    ResourceType.CHAT_MODEL_CONNECTION,
+    ResourceDescriptor(
+        clazz=ResourceName.ChatModel.OLLAMA_CONNECTION,
+        request_timeout=240.0,
+    ),
+)
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```Java
+// Set up the Flink streaming environment and the Agents execution environment.
+StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+env.setParallelism(1);
+AgentsExecutionEnvironment agentsEnv =
+        AgentsExecutionEnvironment.getExecutionEnvironment(env);
+
+// Add Ollama chat model connection to be used by the ParallelChatAgent.
+agentsEnv.addResource(
+        "ollamaChatModelConnection",
+        ResourceType.CHAT_MODEL_CONNECTION,
+        
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_CONNECTION)
+                .addInitialArgument("endpoint", "http://localhost:11434";)
+                .addInitialArgument("requestTimeout", 240)
+                .build());
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+### Create the Agent
+
+Below is the example code for the `ParallelChatAgent`. Two system prompts — 
`PARALLEL_SYSTEM_PROMPT` for the parallel aspect judgments and 
`AGGREGATE_SYSTEM_PROMPT` for the aggregation call — are packaged into 
`ChatRequestEvent`s by `_build_aspect_request` / `buildAspectRequest` and 
`_build_summarize_request` / `buildSummarizeRequest`. The agent defines a chat 
model and four actions: `request_aspect_judgments` stores the review `id` and 
`text` in sensory memory and emits a single generic [...]
+
+{{< tabs "Create the Agent" >}}
+
+{{< tab "Python" >}}
+```python
+ASPECTS: tuple = ("taste", "service")
+N_ASPECTS = len(ASPECTS)
+SENTIMENT_INPUT_EVENT_TYPE = "SentimentInputEvent"
+
+PARALLEL_SYSTEM_PROMPT = (
+    "You are a sentiment analysis assistant. Return JSON: "
+    '{"aspect":"<dimension>", "result":"<positive|negative|not_mentioned>"}'
+    " — no explanation, no extra fields."
+)
+AGGREGATE_SYSTEM_PROMPT = (
+    "You are a summary assistant. Based on the sentiment judgments for two "
+    "dimensions, compose a brief one-line evaluation. Return JSON: "
+    '{"summary":"taste:<positive/negative/not_mentioned>, '
+    'service:<positive/negative/not_mentioned>"} — return only this JSON.'
+)
+
+
+def _build_aspect_request(text: str, aspect: str) -> ChatRequestEvent:
+    """Build a ChatRequestEvent for a single aspect dimension."""
+    return ChatRequestEvent(
+        model="sentiment_model",
+        messages=[
+            ChatMessage(role=MessageRole.SYSTEM, 
content=PARALLEL_SYSTEM_PROMPT),
+            ChatMessage(
+                role=MessageRole.USER,
+                content=f'Judge the "{aspect}" dimension: {text}',
+            ),
+        ],
+        output_schema=OutputSchema(output_schema=AspectResponse),
+    )
+
+
+def _build_summarize_request(text: str, sentiments: Dict[str, str]) -> 
ChatRequestEvent:
+    """Build a ChatRequestEvent for the aggregation phase."""
+    body = (
+        f"Original: {text}\n"
+        + "Judgments: "
+        + " ".join(f"{a}:{sentiments[a]}" for a in ASPECTS)
+    )
+    return ChatRequestEvent(
+        model="sentiment_model",
+        messages=[
+            ChatMessage(role=MessageRole.SYSTEM, 
content=AGGREGATE_SYSTEM_PROMPT),
+            ChatMessage(role=MessageRole.USER, content=body),
+        ],
+        output_schema=OutputSchema(output_schema=SummaryResponse),
+    )
+
+
+class ParallelChatAgent(Agent):
+    """An agent that demonstrates parallel LLM invocations via fan-out of
+    multiple ChatRequestEvent events.
+
+    This agent receives a restaurant review and uses an LLM to judge sentiment
+    along multiple dimensions in parallel, then aggregates the results into a
+    one-line summary with a final LLM call. It handles prompt construction,
+    parallel chat dispatch, response accumulation, and output assembly.
+
+    Event flow:
+      1. InputEvent → request_aspect_judgments → emits SentimentInputEvent
+      2. SentimentInputEvent triggers handlers in parallel:
+           - handle_taste_input   → ChatRequestEvent (taste LLM call)
+           - handle_service_input → ChatRequestEvent (service LLM call)
+      3. Each ChatResponseEvent → handle_response (accumulates aspect results)
+      4. Once all aspects received → aggregation LLM call → OutputEvent
+    """
+
+    @chat_model_setup
+    @staticmethod
+    def sentiment_model() -> ResourceDescriptor:
+        """ChatModel for sentiment analysis."""
+        return ResourceDescriptor(
+            clazz=ResourceName.ChatModel.OLLAMA_SETUP,
+            connection="ollama_server",
+            model=OLLAMA_MODEL,
+            extract_reasoning=True,
+        )
+
+    @action(InputEvent.EVENT_TYPE)
+    @staticmethod
+    def request_aspect_judgments(event: Event, ctx: RunnerContext) -> None:
+        """Process input event and dispatch SentimentInputEvent to aspect 
handlers."""
+        payload = InputEvent.from_event(event).input
+        # Primitive types (int, str) cross the Pemja JVM boundary without 
serialization.
+        ctx.sensory_memory.set("id", payload["id"])
+        ctx.sensory_memory.set("text", payload["text"])
+        ctx.send_event(
+            Event(
+                type=SENTIMENT_INPUT_EVENT_TYPE,
+                attributes={"input_id": payload["id"], "text": 
payload["text"]},
+            )
+        )
+
+    @action(SENTIMENT_INPUT_EVENT_TYPE)
+    @staticmethod
+    def handle_taste_input(event: Event, ctx: RunnerContext) -> None:
+        """Handle taste aspect: build and send ChatRequestEvent for taste 
judgment."""
+        req = _build_aspect_request(event.get_attr("text"), "taste")
+        ctx.sensory_memory.set(f"aspect_map.{req.id}", "taste")
+        ctx.send_event(req)
+
+    @action(SENTIMENT_INPUT_EVENT_TYPE)
+    @staticmethod
+    def handle_service_input(event: Event, ctx: RunnerContext) -> None:
+        """Handle service aspect: build and send ChatRequestEvent for 
service."""
+        req = _build_aspect_request(event.get_attr("text"), "service")
+        ctx.sensory_memory.set(f"aspect_map.{req.id}", "service")
+        ctx.send_event(req)
+
+    @action(ChatResponseEvent.EVENT_TYPE)
+    @staticmethod
+    def handle_response(event: Event, ctx: RunnerContext) -> None:
+        """Process chat response event and send output event."""
+        ...
+```
+
+The complete source including `_build_output_event` and other supporting 
functions can be found in 
[`parallel_chat_agent.py`](https://github.com/apache/flink-agents/blob/main/python/flink_agents/examples/quickstart/agents/parallel_chat_agent.py).
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```Java
+/**
+ * An agent that demonstrates parallel LLM invocations via a broadcast event 
model.
+ *
+ * <p>Event flow:
+ * <ol>
+ *   <li>InputEvent → requestAspectJudgments → emits SentimentInputEvent
+ *   <li>SentimentInputEvent triggers handlers in parallel:
+ *       <ul>
+ *         <li>handleTasteInput → ChatRequestEvent (taste LLM call)
+ *         <li>handleServiceInput → ChatRequestEvent (service LLM call)
+ *       </ul>
+ *   <li>Each ChatResponseEvent → handleResponse (accumulates aspect results)
+ *   <li>Once all aspects received → aggregation LLM call → OutputEvent
+ * </ol>
+ */
+public class ParallelChatAgent extends Agent {
+
+    private static final String[] ASPECTS = {"taste", "service"};
+
+    private static final String SENTIMENT_INPUT_EVENT_TYPE = 
"SentimentInputEvent";
+
+    private static final String PARALLEL_SYSTEM_PROMPT =
+            "You are a sentiment analysis assistant. Return JSON: "
+                    + "{\"aspect\":\"<dimension>\", 
\"result\":\"<positive|negative|not_mentioned>\"}"
+                    + " — no explanation, no extra fields.";
+    private static final String AGGREGATE_SYSTEM_PROMPT =
+            "You are a summary assistant. Based on the sentiment judgments for 
two "
+                    + "dimensions, compose a brief one-line evaluation. Return 
JSON: "
+                    + "{\"summary\":\"taste:<positive/negative/not_mentioned>, 
"
+                    + "service:<positive/negative/not_mentioned>\"} — return 
only this JSON.";
+
+    @ChatModelSetup
+    public static ResourceDescriptor sentimentModel() {
+        return 
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP)
+                .addInitialArgument("connection", "ollamaChatModelConnection")
+                .addInitialArgument("model", OLLAMA_MODEL)
+                .addInitialArgument("extract_reasoning", true)
+                .build();
+    }
+
+    private static ChatRequestEvent buildAspectRequest(String text, String 
aspect) {
+        List<ChatMessage> messages =
+                List.of(
+                        new ChatMessage(MessageRole.SYSTEM, 
PARALLEL_SYSTEM_PROMPT),
+                        new ChatMessage(
+                                MessageRole.USER,
+                                "Judge the \"" + aspect + "\" dimension: " + 
text));
+        return new ChatRequestEvent(
+                "sentimentModel", messages, 
CustomTypesAndResources.AspectResponse.class);
+    }
+
+    private static ChatRequestEvent buildSummarizeRequest(
+            String text, Map<String, String> sentiments) {
+        StringJoiner sj = new StringJoiner(" ");
+        for (String aspect : ASPECTS) {
+            sj.add(aspect + ":" + sentiments.get(aspect));
+        }
+        String body = "Original: " + text + "\nJudgments: " + sj;
+        List<ChatMessage> messages =
+                List.of(
+                        new ChatMessage(MessageRole.SYSTEM, 
AGGREGATE_SYSTEM_PROMPT),
+                        new ChatMessage(MessageRole.USER, body));
+        return new ChatRequestEvent(
+                "sentimentModel", messages, 
CustomTypesAndResources.SummaryResponse.class);
+    }
+
+    /** Process input event and dispatch a SentimentInputEvent for each aspect 
handler. */
+    @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+    public static void requestAspectJudgments(Event event, RunnerContext ctx) 
throws Exception {
+        CustomTypesAndResources.SentimentRequest request =
+                (CustomTypesAndResources.SentimentRequest) 
InputEvent.fromEvent(event).getInput();
+        ctx.getSensoryMemory().set("id", request.getId());
+        ctx.getSensoryMemory().set("text", request.getText());
+        ctx.sendEvent(
+                new Event(
+                        SENTIMENT_INPUT_EVENT_TYPE,
+                        Map.of("input_id", request.getId(), "text", 
request.getText())));
+    }
+
+    /** Handle taste aspect: build and send ChatRequestEvent for taste 
judgment. */
+    @Action(listenEventTypes = {SENTIMENT_INPUT_EVENT_TYPE})
+    public static void handleTasteInput(Event event, RunnerContext ctx) throws 
Exception {
+        ChatRequestEvent req = buildAspectRequest((String) 
event.getAttr("text"), "taste");
+        ctx.getSensoryMemory().set("aspect_map." + req.getId(), "taste");
+        ctx.sendEvent(req);
+    }
+
+    /** Handle service aspect: build and send ChatRequestEvent for service 
judgment. */
+    @Action(listenEventTypes = {SENTIMENT_INPUT_EVENT_TYPE})
+    public static void handleServiceInput(Event event, RunnerContext ctx) 
throws Exception {
+        ChatRequestEvent req = buildAspectRequest((String) 
event.getAttr("text"), "service");
+        ctx.getSensoryMemory().set("aspect_map." + req.getId(), "service");
+        ctx.sendEvent(req);
+    }
+
+    @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+    public static void handleResponse(Event event, RunnerContext ctx) throws 
Exception {
+        ...
+    }
+}
+```
+
+Other private helpers (`buildOutputEvent`) are omitted above; the full source 
is at 
[`ParallelChatAgent.java`](https://github.com/apache/flink-agents/blob/main/examples/src/main/java/org/apache/flink/agents/examples/agents/ParallelChatAgent.java).
+{{< /tab >}}
+
+{{< /tabs >}}
+
+{{< hint warning >}}
+**Constraints when using parallel actions:**
+- **Dispatch action must exit immediately after sending events.** The action 
that emits `SentimentInputEvent` (i.e. `request_aspect_judgments`) should 
return right after calling `ctx.send_event(...)`. Do not `await`, block, or 
perform further business logic in the same action — the framework needs control 
back to schedule the parallel chat actions.
+- **Action handlers on the same key execute sequentially.** Multiple action 
invocations triggered by the same key are processed one at a time, not 
concurrently. This means `handle_taste_input` and `handle_service_input` are 
called in sequence. Each handler writes a single path-based entry 
(`aspect_map.<request_id>`) to sensory memory without reading back, so there is 
no conflict between the two. The actual LLM calls dispatched by these handlers 
still execute in parallel.
+- **Response handlers on the same key execute sequentially.** Multiple 
`ChatResponseEvent` triggers for the same key are processed one at a time, not 
concurrently. This means the path-based writes to `sentiments.<aspect>` in 
`handle_response` are safe and will not suffer from concurrent overwrites.
+{{< /hint >}}
+
+### Integrate the Agent with Flink
+
+Create the input data, use the `ParallelChatAgent` to analyze the review with 
parallel LLM calls, and print the results.
+
+{{< tabs "Integrate the Agent with Flink" >}}
+
+{{< tab "Python" >}}
+```python
+# Create input stream with a single restaurant review.
+input_stream = env.from_collection(
+    collection=[{"id": 1, "text": INPUT_TEXT}],
+)
+
+# Use the ParallelChatAgent to analyze the review with parallel LLM calls.
+output_stream = (
+    agents_env.from_datastream(
+        input=input_stream, key_selector=lambda x: x["id"]
+    )
+    .apply(ParallelChatAgent())
+    .to_datastream()
+)
+
+# Print the analysis results to stdout.
+output_stream.print()
+
+# Execute the Flink pipeline.
+agents_env.execute()
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```Java
+// Create input stream with a single restaurant review.
+DataStream<SentimentRequest> inputStream =
+        env.fromElements(new SentimentRequest(1, 
ParallelChatAgent.INPUT_TEXT));
+
+// Use the ParallelChatAgent to analyze the review with parallel LLM calls.
+DataStream<Object> outputStream =
+        agentsEnv
+                .fromDataStream(inputStream, new SentimentKeySelector())
+                .apply(new ParallelChatAgent())
+                .toDataStream();
+
+// Print the analysis results to stdout.
+outputStream.print();
+
+// Execute the Flink pipeline.
+agentsEnv.execute();
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+## Run the Example
+
+### Prerequisites
+
+* Unix-like environment (we use Linux, Mac OS X, Cygwin, WSL)
+* Git
+* Java 11+ (Java 21+ recommended for parallel execution on the Java side)
+* Python 3.10, 3.11 or 3.12
+
+### Preparation
+
+#### Prepare Flink and Flink Agents
+
+Follow the [installation]({{< ref "docs/get-started/installation" >}}) 
instructions to setup Flink and the Flink Agents.
+
+#### Clone the Flink Agents Repository (if not done already)
+
+```bash
+git clone https://github.com/apache/flink-agents.git
+cd flink-agents
+```
+{{< hint info >}}
+For python examples, you can skip this step and submit the python file in 
installed flink-agents wheel.
+{{< /hint >}}
+
+#### Deploy a Standalone Flink Cluster
+
+You can deploy a standalone Flink cluster in your local environment with the 
following command.
+
+{{< tabs "Deploy a Standalone Flink Cluster" >}}
+
+{{< tab "Python" >}}
+```bash
+export PYTHONPATH=$(python -c 'import sysconfig; 
print(sysconfig.get_paths()["purelib"])')
+$FLINK_HOME/bin/start-cluster.sh
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+1. Build Flink Agents from source to generate example jar. See 
[installation]({{< ref "docs/get-started/installation" >}}) for more details.
+2. Start the Flink cluster
+    ```bash
+    $FLINK_HOME/bin/start-cluster.sh
+    ```
+
+{{< hint info >}}
+To run example on JDK 21+, append jvm option 
`--add-exports=java.base/jdk.internal.vm=ALL-UNNAMED` to 
[env.java.opts.all](https://nightlies.apache.org/flink/flink-docs-stable/docs/deployment/config/#env-java-opts-all)
 in `$FLINK_HOME/conf/config.yaml` before start the flink cluster.
+{{< /hint >}}
+{{< /tab >}}
+
+{{< /tabs >}}
+You can refer to the [local 
cluster](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/try-flink/local_installation/#starting-and-stopping-a-local-cluster)
 instructions for more detailed step.
+
+{{< hint info >}}
+If you can't navigate to the web UI at [localhost:8081](localhost:8081), you 
can find the reason in `$FLINK_HOME/log`. If the reason is port conflict, you 
can change the port in `$FLINK_HOME/conf/config.yaml`.
+{{< /hint >}}
+
+#### Prepare Ollama
+
+Download and install Ollama from the official 
[website](https://ollama.com/download).
+
+{{< hint info >}}
+Ollama server **0.9.0** or higher is required.
+{{< /hint >}}
+
+Then pull the qwen3:1.7b model, which is required by the parallel LLM example
+
+```bash
+ollama pull qwen3:1.7b
+```
+
+### Submit Flink Agents Job to Standalone Flink Cluster
+
+#### Submit to Flink Cluster
+
+{{< tabs "Submit to Flink Cluster" >}}
+
+{{< tab "Python" >}}
+```bash
+export PYTHONPATH=$(python -c 'import sysconfig; 
print(sysconfig.get_paths()["purelib"])')
+
+# Run parallel chat request example
+$FLINK_HOME/bin/flink run -py 
./flink-agents/python/flink_agents/examples/quickstart/parallel_chat_request_example.py
+# or submit the example python file in installed flink-agents wheel
+$FLINK_HOME/bin/flink run -py  
$PYTHONPATH/flink_agents/examples/quickstart/parallel_chat_request_example.py
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```bash
+$FLINK_HOME/bin/flink run -c 
org.apache.flink.agents.examples.ParallelChatRequestExample 
./flink-agents/examples/target/flink-agents-examples-$VERSION.jar
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+Now you should see a Flink job submitted to the Flink Cluster in Flink web UI 
[localhost:8081](
+localhost:8081)
+
+After a few minutes, you can check for the output in the TaskManager output 
log.
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/ParallelChatRequestExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/ParallelChatRequestExample.java
new file mode 100644
index 00000000..edc54050
--- /dev/null
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/ParallelChatRequestExample.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+package org.apache.flink.agents.examples;
+
+import org.apache.flink.agents.api.AgentsExecutionEnvironment;
+import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.apache.flink.agents.api.resource.ResourceName;
+import org.apache.flink.agents.api.resource.ResourceType;
+import 
org.apache.flink.agents.examples.agents.CustomTypesAndResources.SentimentKeySelector;
+import 
org.apache.flink.agents.examples.agents.CustomTypesAndResources.SentimentRequest;
+import org.apache.flink.agents.examples.agents.ParallelChatAgent;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+
+/**
+ * Java example demonstrating parallel LLM invocations via multi-action 
fan-out.
+ *
+ * <p>This example demonstrates how to use the Flink Agents to analyze a 
restaurant review by
+ * fanning out multiple parallel LLM calls — one per sentiment dimension — and 
aggregating the
+ * results with a final LLM call. This serves as a minimal, end-to-end example 
of integrating
+ * parallel LLM-powered agents with Flink streaming jobs.
+ */
+public class ParallelChatRequestExample {
+
+    /** Runs the example pipeline. */
+    public static void main(String[] args) throws Exception {
+        // Set up the Flink streaming environment and the Agents execution 
environment.
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env);
+
+        // Add Ollama chat model connection to be used by the 
ParallelChatAgent.
+        agentsEnv.addResource(
+                "ollamaChatModelConnection",
+                ResourceType.CHAT_MODEL_CONNECTION,
+                
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_CONNECTION)
+                        .addInitialArgument("endpoint", 
"http://localhost:11434";)
+                        .addInitialArgument("requestTimeout", 240)
+                        .build());
+
+        // Create input stream with a single restaurant review.
+        DataStream<SentimentRequest> inputStream =
+                env.fromElements(new SentimentRequest(1, 
ParallelChatAgent.INPUT_TEXT));
+
+        // Use the ParallelChatAgent to analyze the review with parallel LLM 
calls.
+        DataStream<Object> outputStream =
+                agentsEnv
+                        .fromDataStream(inputStream, new 
SentimentKeySelector())
+                        .apply(new ParallelChatAgent())
+                        .toDataStream();
+
+        // Print the analysis results to stdout.
+        outputStream.print();
+
+        // Execute the Flink pipeline.
+        agentsEnv.execute();
+    }
+}
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/CustomTypesAndResources.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/CustomTypesAndResources.java
index 2fb1da8c..2447fa18 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/CustomTypesAndResources.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/CustomTypesAndResources.java
@@ -26,7 +26,9 @@ import org.apache.flink.agents.api.chat.messages.MessageRole;
 import org.apache.flink.agents.api.prompt.Prompt;
 import org.apache.flink.agents.api.resource.ResourceDescriptor;
 import org.apache.flink.agents.api.resource.ResourceName;
+import org.apache.flink.api.java.functions.KeySelector;
 
+import java.io.Serializable;
 import java.util.Arrays;
 import java.util.List;
 
@@ -224,6 +226,66 @@ public class CustomTypesAndResources {
         }
     }
 
+    /** Single input record carrying an id and text for parallel sentiment 
analysis. */
+    public static class SentimentRequest implements Serializable {
+        private static final long serialVersionUID = 1L;
+        private final int id;
+        private final String text;
+
+        public SentimentRequest(int id, String text) {
+            this.id = id;
+            this.text = text;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public String getText() {
+            return text;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("SentimentRequest{id=%d, text='%s'}", id, 
text);
+        }
+    }
+
+    /** Key selector that extracts the id field from a {@link 
SentimentRequest}. */
+    public static class SentimentKeySelector implements 
KeySelector<SentimentRequest, Integer> {
+        @Override
+        public Integer getKey(SentimentRequest request) {
+            return request.getId();
+        }
+    }
+
+    /** LLM response for a single aspect judgment. */
+    public static class AspectResponse implements Serializable {
+        private static final long serialVersionUID = 1L;
+        public String aspect;
+        public String result;
+
+        public AspectResponse() {}
+
+        @Override
+        public String toString() {
+            return String.format("AspectResponse{aspect='%s', result='%s'}", 
aspect, result);
+        }
+    }
+
+    /** LLM response for the aggregation phase. */
+    public static class SummaryResponse implements Serializable {
+        private static final long serialVersionUID = 1L;
+        public String summary;
+
+        public SummaryResponse() {}
+
+        @Override
+        public String toString() {
+            return String.format("SummaryResponse{summary='%s'}", summary);
+        }
+    }
+
     /** Provides a summary of review data including suggestions for 
improvement. */
     @JsonSerialize
     @JsonDeserialize
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/ParallelChatAgent.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/ParallelChatAgent.java
new file mode 100644
index 00000000..24c85214
--- /dev/null
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/ParallelChatAgent.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+package org.apache.flink.agents.examples.agents;
+
+import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.OutputEvent;
+import org.apache.flink.agents.api.agents.Agent;
+import org.apache.flink.agents.api.annotation.Action;
+import org.apache.flink.agents.api.annotation.ChatModelSetup;
+import org.apache.flink.agents.api.chat.messages.ChatMessage;
+import org.apache.flink.agents.api.chat.messages.MessageRole;
+import org.apache.flink.agents.api.context.RunnerContext;
+import org.apache.flink.agents.api.event.ChatRequestEvent;
+import org.apache.flink.agents.api.event.ChatResponseEvent;
+import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.apache.flink.agents.api.resource.ResourceName;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+import static org.apache.flink.agents.api.agents.Agent.STRUCTURED_OUTPUT;
+
+/**
+ * An agent that demonstrates parallel LLM invocations via fan-out of multiple 
{@link
+ * ChatRequestEvent} events.
+ *
+ * <p>This agent receives a restaurant review and uses an LLM to judge 
sentiment along multiple
+ * dimensions (taste / service) in parallel, then aggregates the results into 
a one-line summary
+ * with a final LLM call. It handles prompt construction, parallel chat 
dispatch, response
+ * accumulation, and output assembly.
+ *
+ * <p>Event flow:
+ *
+ * <ol>
+ *   <li>InputEvent → requestAspectJudgments → emits SentimentInputEvent
+ *   <li>SentimentInputEvent triggers handlers in parallel:
+ *       <ul>
+ *         <li>handleTasteInput → ChatRequestEvent (taste LLM call)
+ *         <li>handleServiceInput → ChatRequestEvent (service LLM call)
+ *       </ul>
+ *   <li>Each ChatResponseEvent → handleResponse (accumulates aspect results)
+ *   <li>Once all aspects received → aggregation LLM call → OutputEvent
+ * </ol>
+ *
+ * <p><b>JDK version note:</b> On JDK 21+, the framework uses the Continuation 
API to execute
+ * concurrent chat actions in parallel, so the wall clock time is roughly 
"slowest single branch +
+ * aggregation call". On JDK &lt; 21, the framework silently falls back to 
sequential execution —
+ * the result is identical, but the LLM calls run one after another.
+ */
+public class ParallelChatAgent extends Agent {
+
+    /** Ollama model name, configurable via environment variable. */
+    public static final String OLLAMA_MODEL =
+            System.getenv().getOrDefault("PARALLEL_CHAT_OLLAMA_MODEL", 
"qwen3:1.7b");
+
+    /** Input text for the demo. */
+    public static final String INPUT_TEXT = "The food here is great, but the 
service is too slow";
+
+    private static final String[] ASPECTS = {"taste", "service"};
+
+    private static final String SENTIMENT_INPUT_EVENT_TYPE = 
"SentimentInputEvent";
+
+    private static final String PARALLEL_SYSTEM_PROMPT =
+            "You are a sentiment analysis assistant. Return JSON: "
+                    + "{\"aspect\":\"<dimension>\", 
\"result\":\"<positive|negative|not_mentioned>\"}"
+                    + " — no explanation, no extra fields.";
+    private static final String AGGREGATE_SYSTEM_PROMPT =
+            "You are a summary assistant. Based on the sentiment judgments for 
two "
+                    + "dimensions, compose a brief one-line evaluation. Return 
JSON: "
+                    + "{\"summary\":\"taste:<positive/negative/not_mentioned>, 
"
+                    + "service:<positive/negative/not_mentioned>\"} — return 
only this JSON.";
+
+    @ChatModelSetup
+    public static ResourceDescriptor sentimentModel() {
+        return 
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP)
+                .addInitialArgument("connection", "ollamaChatModelConnection")
+                .addInitialArgument("model", OLLAMA_MODEL)
+                .addInitialArgument("extract_reasoning", true)
+                .build();
+    }
+
+    private static ChatRequestEvent buildAspectRequest(String text, String 
aspect) {
+        List<ChatMessage> messages =
+                List.of(
+                        new ChatMessage(MessageRole.SYSTEM, 
PARALLEL_SYSTEM_PROMPT),
+                        new ChatMessage(
+                                MessageRole.USER,
+                                "Judge the \"" + aspect + "\" dimension: " + 
text));
+        return new ChatRequestEvent(
+                "sentimentModel", messages, 
CustomTypesAndResources.AspectResponse.class);
+    }
+
+    private static ChatRequestEvent buildSummarizeRequest(
+            String text, Map<String, String> sentiments) {
+        StringJoiner sj = new StringJoiner(" ");
+        for (String aspect : ASPECTS) {
+            sj.add(aspect + ":" + sentiments.get(aspect));
+        }
+        String body = "Original: " + text + "\nJudgments: " + sj;
+        List<ChatMessage> messages =
+                List.of(
+                        new ChatMessage(MessageRole.SYSTEM, 
AGGREGATE_SYSTEM_PROMPT),
+                        new ChatMessage(MessageRole.USER, body));
+        return new ChatRequestEvent(
+                "sentimentModel", messages, 
CustomTypesAndResources.SummaryResponse.class);
+    }
+
+    private static OutputEvent buildOutputEvent(
+            int id, String text, CustomTypesAndResources.SummaryResponse 
parsed) {
+        Map<String, Object> output = new HashMap<>();
+        output.put("id", id);
+        output.put("text", text);
+        output.put("summary", parsed.summary);
+        return new OutputEvent(output);
+    }
+
+    /** Process input event and dispatch a SentimentInputEvent for each aspect 
handler. */
+    @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+    public static void requestAspectJudgments(Event event, RunnerContext ctx) 
throws Exception {
+        InputEvent inputEvent = InputEvent.fromEvent(event);
+        CustomTypesAndResources.SentimentRequest request =
+                (CustomTypesAndResources.SentimentRequest) 
inputEvent.getInput();
+        ctx.getSensoryMemory().set("id", request.getId());
+        ctx.getSensoryMemory().set("text", request.getText());
+        ctx.sendEvent(
+                new Event(
+                        SENTIMENT_INPUT_EVENT_TYPE,
+                        Map.of("input_id", request.getId(), "text", 
request.getText())));
+    }
+
+    /** Handle taste aspect: build and send ChatRequestEvent for taste 
judgment. */
+    @Action(listenEventTypes = {SENTIMENT_INPUT_EVENT_TYPE})
+    public static void handleTasteInput(Event event, RunnerContext ctx) throws 
Exception {
+        ChatRequestEvent req = buildAspectRequest((String) 
event.getAttr("text"), "taste");
+        ctx.getSensoryMemory().set("aspect_map." + req.getId(), "taste");
+        ctx.sendEvent(req);
+    }
+
+    /** Handle service aspect: build and send ChatRequestEvent for service 
judgment. */
+    @Action(listenEventTypes = {SENTIMENT_INPUT_EVENT_TYPE})
+    public static void handleServiceInput(Event event, RunnerContext ctx) 
throws Exception {
+        ChatRequestEvent req = buildAspectRequest((String) 
event.getAttr("text"), "service");
+        ctx.getSensoryMemory().set("aspect_map." + req.getId(), "service");
+        ctx.sendEvent(req);
+    }
+
+    /** Process chat response event. */
+    @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+    public static void handleResponse(Event event, RunnerContext ctx) throws 
Exception {
+        ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
+        Object parsed = 
chatResponse.getResponse().getExtraArgs().get(STRUCTURED_OUTPUT);
+
+        if (parsed instanceof CustomTypesAndResources.SummaryResponse) {
+            CustomTypesAndResources.SummaryResponse summary =
+                    (CustomTypesAndResources.SummaryResponse) parsed;
+            int id = (int) ctx.getSensoryMemory().get("id").getValue();
+            String text = (String) 
ctx.getSensoryMemory().get("text").getValue();
+            ctx.sendEvent(buildOutputEvent(id, text, summary));
+            return;
+        }
+
+        CustomTypesAndResources.AspectResponse aspectResponse =
+                (CustomTypesAndResources.AspectResponse) parsed;
+        String aspect =
+                (String)
+                        ctx.getSensoryMemory()
+                                .get("aspect_map." + 
chatResponse.getRequestId())
+                                .getValue();
+        ctx.getSensoryMemory().set("sentiments." + aspect, 
aspectResponse.result);
+        boolean allReceived = true;
+        for (String a : ASPECTS) {
+            if (!ctx.getSensoryMemory().isExist("sentiments." + a)) {
+                allReceived = false;
+                break;
+            }
+        }
+        if (allReceived) {
+            String text = (String) 
ctx.getSensoryMemory().get("text").getValue();
+            Map<String, String> sentiments = new HashMap<>();
+            for (String a : ASPECTS) {
+                sentiments.put(
+                        a, (String) ctx.getSensoryMemory().get("sentiments." + 
a).getValue());
+            }
+            ctx.sendEvent(buildSummarizeRequest(text, sentiments));
+        }
+    }
+}
diff --git 
a/python/flink_agents/examples/quickstart/agents/custom_types_and_resources.py 
b/python/flink_agents/examples/quickstart/agents/custom_types_and_resources.py
index 17b5ea9d..b12087a0 100644
--- 
a/python/flink_agents/examples/quickstart/agents/custom_types_and_resources.py
+++ 
b/python/flink_agents/examples/quickstart/agents/custom_types_and_resources.py
@@ -196,6 +196,20 @@ class ProductReviewAnalysisRes(BaseModel):
     reasons: list[str]
 
 
+# Custom types for parallel chat agent.
+class AspectResponse(BaseModel):
+    """LLM response for a single aspect judgment."""
+
+    aspect: str
+    result: str
+
+
+class SummaryResponse(BaseModel):
+    """LLM response for the aggregation phase."""
+
+    summary: str
+
+
 # ollama chat model connection descriptor
 ollama_server_descriptor = ResourceDescriptor(
     clazz=ResourceName.ChatModel.OLLAMA_CONNECTION, request_timeout=120
diff --git 
a/python/flink_agents/examples/quickstart/agents/parallel_chat_agent.py 
b/python/flink_agents/examples/quickstart/agents/parallel_chat_agent.py
new file mode 100644
index 00000000..6d98cb2a
--- /dev/null
+++ b/python/flink_agents/examples/quickstart/agents/parallel_chat_agent.py
@@ -0,0 +1,173 @@
+################################################################################
+#  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.
+#################################################################################
+import os
+from typing import Dict
+
+from flink_agents.api.agents.agent import STRUCTURED_OUTPUT, Agent
+from flink_agents.api.agents.types import OutputSchema
+from flink_agents.api.chat_message import ChatMessage, MessageRole
+from flink_agents.api.decorators import action, chat_model_setup
+from flink_agents.api.events.chat_event import ChatRequestEvent, 
ChatResponseEvent
+from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.resource import ResourceDescriptor, ResourceName
+from flink_agents.api.runner_context import RunnerContext
+from flink_agents.examples.quickstart.agents.custom_types_and_resources import 
(
+    AspectResponse,
+    SummaryResponse,
+)
+
+OLLAMA_MODEL = os.environ.get("PARALLEL_CHAT_OLLAMA_MODEL", "qwen3:1.7b")
+
+INPUT_TEXT = "The food here is great, but the service is too slow"
+SENTIMENT_INPUT_EVENT_TYPE = "SentimentInputEvent"
+ASPECTS: tuple = ("taste", "service")
+N_ASPECTS = len(ASPECTS)
+
+PARALLEL_SYSTEM_PROMPT = (
+    "You are a sentiment analysis assistant. Return JSON: "
+    '{"aspect":"<dimension>", "result":"<positive|negative|not_mentioned>"}'
+    " — no explanation, no extra fields."
+)
+AGGREGATE_SYSTEM_PROMPT = (
+    "You are a summary assistant. Based on the sentiment judgments for two "
+    "dimensions, compose a brief one-line evaluation. Return JSON: "
+    '{"summary":"taste:<positive/negative/not_mentioned>, '
+    'service:<positive/negative/not_mentioned>"} — return only this JSON.'
+)
+
+
+def _build_aspect_request(text: str, aspect: str) -> ChatRequestEvent:
+    """Build a ChatRequestEvent for a single aspect dimension."""
+    return ChatRequestEvent(
+        model="sentiment_model",
+        messages=[
+            ChatMessage(role=MessageRole.SYSTEM, 
content=PARALLEL_SYSTEM_PROMPT),
+            ChatMessage(
+                role=MessageRole.USER,
+                content=f'Judge the "{aspect}" dimension: {text}',
+            ),
+        ],
+        output_schema=OutputSchema(output_schema=AspectResponse),
+    )
+
+
+def _build_summarize_request(text: str, sentiments: Dict[str, str]) -> 
ChatRequestEvent:
+    """Build a ChatRequestEvent for the aggregation phase."""
+    body = (
+        f"Original: {text}\n"
+        + "Judgments: "
+        + " ".join(f"{a}:{sentiments[a]}" for a in ASPECTS)
+    )
+    return ChatRequestEvent(
+        model="sentiment_model",
+        messages=[
+            ChatMessage(role=MessageRole.SYSTEM, 
content=AGGREGATE_SYSTEM_PROMPT),
+            ChatMessage(role=MessageRole.USER, content=body),
+        ],
+        output_schema=OutputSchema(output_schema=SummaryResponse),
+    )
+
+
+def _build_output_event(row_id: int, text: str, parsed: SummaryResponse) -> 
OutputEvent:
+    """Build the final OutputEvent from the aggregated row."""
+    return OutputEvent(output={"id": row_id, "text": text, "summary": 
parsed.summary})
+
+
+class ParallelChatAgent(Agent):
+    """An agent that demonstrates parallel LLM invocations via fan-out of
+    multiple ChatRequestEvent events.
+
+    This agent receives a restaurant review and uses an LLM to judge sentiment
+    along multiple dimensions in parallel, then aggregates the results into a
+    one-line summary with a final LLM call. It handles prompt construction,
+    parallel chat dispatch, response accumulation, and output assembly.
+
+    Event flow:
+      1. InputEvent → request_aspect_judgments → emits SentimentInputEvent
+      2. SentimentInputEvent triggers handlers in parallel:
+           - handle_taste_input   → ChatRequestEvent (taste LLM call)
+           - handle_service_input → ChatRequestEvent (service LLM call)
+      3. Each ChatResponseEvent → handle_response (accumulates aspect results)
+      4. Once all aspects received → aggregation LLM call → OutputEvent
+    """
+
+    @chat_model_setup
+    @staticmethod
+    def sentiment_model() -> ResourceDescriptor:
+        """ChatModel for sentiment analysis."""
+        return ResourceDescriptor(
+            clazz=ResourceName.ChatModel.OLLAMA_SETUP,
+            connection="ollama_server",
+            model=OLLAMA_MODEL,
+            extract_reasoning=True,
+        )
+
+    @action(InputEvent.EVENT_TYPE)
+    @staticmethod
+    def request_aspect_judgments(event: Event, ctx: RunnerContext) -> None:
+        """Process input event and dispatch SentimentInputEvent to aspect 
handlers."""
+        payload = InputEvent.from_event(event).input
+        # Primitive types (int, str) cross the Pemja JVM boundary without 
serialization.
+        ctx.sensory_memory.set("id", payload["id"])
+        ctx.sensory_memory.set("text", payload["text"])
+        ctx.send_event(
+            Event(
+                type=SENTIMENT_INPUT_EVENT_TYPE,
+                attributes={"input_id": payload["id"], "text": 
payload["text"]},
+            )
+        )
+
+    @action(SENTIMENT_INPUT_EVENT_TYPE)
+    @staticmethod
+    def handle_taste_input(event: Event, ctx: RunnerContext) -> None:
+        """Handle taste aspect: build and send ChatRequestEvent for taste 
judgment."""
+        req = _build_aspect_request(event.get_attr("text"), "taste")
+        ctx.sensory_memory.set(f"aspect_map.{req.id}", "taste")
+        ctx.send_event(req)
+
+    @action(SENTIMENT_INPUT_EVENT_TYPE)
+    @staticmethod
+    def handle_service_input(event: Event, ctx: RunnerContext) -> None:
+        """Handle service aspect: build and send ChatRequestEvent for 
service."""
+        req = _build_aspect_request(event.get_attr("text"), "service")
+        ctx.sensory_memory.set(f"aspect_map.{req.id}", "service")
+        ctx.send_event(req)
+
+    @action(ChatResponseEvent.EVENT_TYPE)
+    @staticmethod
+    def handle_response(event: Event, ctx: RunnerContext) -> None:
+        """Process chat response event and send output event."""
+        response_event = ChatResponseEvent.from_event(event)
+        parsed = response_event.response.extra_args[STRUCTURED_OUTPUT]
+        if isinstance(parsed, dict):
+            parsed = SummaryResponse(**parsed) if "summary" in parsed else 
AspectResponse(**parsed)
+        if isinstance(parsed, SummaryResponse):
+            ctx.send_event(
+                _build_output_event(
+                    ctx.sensory_memory.get("id"),
+                    ctx.sensory_memory.get("text"),
+                    parsed,
+                )
+            )
+            return
+        aspect = 
ctx.sensory_memory.get(f"aspect_map.{response_event.request_id}")
+        ctx.sensory_memory.set(f"sentiments.{aspect}", parsed.result)
+        if all(ctx.sensory_memory.is_exist(f"sentiments.{a}") for a in 
ASPECTS):
+            text = ctx.sensory_memory.get("text")
+            sentiments = {a: ctx.sensory_memory.get(f"sentiments.{a}") for a 
in ASPECTS}
+            ctx.send_event(_build_summarize_request(text, sentiments))
diff --git 
a/python/flink_agents/examples/quickstart/parallel_chat_request_example.py 
b/python/flink_agents/examples/quickstart/parallel_chat_request_example.py
new file mode 100644
index 00000000..083709aa
--- /dev/null
+++ b/python/flink_agents/examples/quickstart/parallel_chat_request_example.py
@@ -0,0 +1,74 @@
+################################################################################
+#  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.
+#################################################################################
+from pyflink.datastream import StreamExecutionEnvironment
+
+from flink_agents.api.execution_environment import AgentsExecutionEnvironment
+from flink_agents.api.resource import ResourceDescriptor, ResourceName, 
ResourceType
+from flink_agents.examples.quickstart.agents.parallel_chat_agent import (
+    INPUT_TEXT,
+    ParallelChatAgent,
+)
+
+
+def main() -> None:
+    """Main function for the parallel chat request quickstart example.
+
+    This example demonstrates how to use Flink Agents to analyze a restaurant
+    review by fanning out multiple parallel LLM calls — one per sentiment
+    dimension — and aggregating the results with a final LLM call. This serves
+    as a minimal, end-to-end example of integrating parallel LLM-powered agents
+    with Flink streaming jobs.
+    """
+    # 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)
+
+    # Add Ollama chat model connection to be used by the ParallelChatAgent.
+    agents_env.add_resource(
+        "ollama_server",
+        ResourceType.CHAT_MODEL_CONNECTION,
+        ResourceDescriptor(
+            clazz=ResourceName.ChatModel.OLLAMA_CONNECTION,
+            request_timeout=240.0,
+        ),
+    )
+
+    # Create input stream with a single restaurant review.
+    input_stream = env.from_collection(
+        collection=[{"id": 1, "text": INPUT_TEXT}],
+    )
+
+    # Use the ParallelChatAgent to analyze the review with parallel LLM calls.
+    output_stream = (
+        agents_env.from_datastream(
+            input=input_stream, key_selector=lambda x: x["id"]
+        )
+        .apply(ParallelChatAgent())
+        .to_datastream()
+    )
+
+    # Print the analysis results to stdout.
+    output_stream.print()
+
+    # Execute the Flink pipeline.
+    agents_env.execute()
+
+
+if __name__ == "__main__":
+    main()

Reply via email to