diff --git a/docs/content/docs/development/skills.md b/docs/content/docs/development/skills.md new file mode 100644 index 000000000..d07c84ed1 --- /dev/null +++ b/docs/content/docs/development/skills.md @@ -0,0 +1,222 @@ +--- +title: Skills +weight: 10 +type: docs +--- + + +## Overview + +A **Skill** is a self-contained package of instructions, and optionally scripts and reference files, that teaches the agent how to perform a specialized task. A skill is just a directory containing a `SKILL.md` file. Flink Agents discovers the skills you declare, lets the agent decide which one is relevant to the current request, and loads its full instructions only when needed. + +Skills follow a **progressive disclosure** model, so that providing the agent with many capabilities does not bloat every request: + +1. **Discovery** — at startup, only each skill's `name` and `description` are injected into the system prompt (a few dozen tokens per skill), so the agent knows what is available. +2. **Activation** — when the agent judges a skill relevant, it calls the built-in `load_skill` tool to read the full `SKILL.md` instructions into the context. +3. **Execution** — the agent follows the loaded instructions, running any bundled scripts or shell commands through the built-in `bash` tool, and reading additional reference files only on demand. + +Skills are a good fit when a capability is best described as a procedure (a runbook the agent follows) rather than a single function call. For a single, well-typed operation, prefer a [tool]({{< ref "docs/development/tool_use" >}}) instead. + +## Skill Format + +A skill is a directory whose name matches the skill, containing a `SKILL.md` file with YAML frontmatter and a Markdown body: + +````markdown +--- +name: math-calculator +description: Calculate mathematical expressions using shell commands. Use when the user asks to perform arithmetic like addition, subtraction, multiplication, division, or powers. +license: Apache-2.0 +compatibility: Requires bash with bc (basic calculator) +--- + +# Math Calculator Skill + +## When to Use +Use this skill whenever the user asks to evaluate a numeric expression. + +## Method +Evaluate expressions with the `bc` calculator: + +```bash +echo "(2 + 3) * 4" | bc +# Output: 20 +``` +```` + +**Frontmatter fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Skill identifier. 1–64 characters, lowercase letters, numbers and hyphens only (no leading/trailing hyphen). Must match the value referenced in the chat model's `skills` list. | +| `description` | Yes | 1–1024 characters. Loaded at discovery time — write it so the agent can decide *when* to use the skill. State both what it does and when to use it. | +| `license` | No | License of the skill. | +| `compatibility` | No | Free-text note on runtime requirements (e.g. required commands), up to 500 characters. | + +The Markdown body is the full instruction set loaded on activation. It may reference bundled files using paths relative to the skill directory (for example `python scripts/gen_joke.py`); those scripts and reference files are loaded only when the agent actually needs them. + +A skill source is a directory holding one or more such skill subdirectories (or a `.zip` of that layout): + +``` +skills/ +├── math-calculator/ +│ └── SKILL.md +└── joke-generator/ + ├── SKILL.md + └── scripts/ + └── gen_joke.py +``` + +## Declare Skills in an Agent + +Declare where to load skills from with the `@skills`/`@Skills` decorator/annotation. The method returns a `Skills` resource built with one of its factory methods. + +{{< tabs "Declare Skills in Agent" >}} + +{{< tab "Python" >}} +```python +from flink_agents.api.agents.agent import Agent +from flink_agents.api.decorators import skills +from flink_agents.api.skills import Skills + + +class MathAgent(Agent): + + @skills + @staticmethod + def my_skills() -> Skills: + # Load all skill subdirectories under a local directory. + return Skills.from_local_dir("/path/to/skills") +``` +{{< /tab >}} + +{{< tab "Java" >}} +```java +import org.apache.flink.agents.api.agents.Agent; +// The @Skills annotation and the Skills resource share a simple name but live +// in different packages, so fully-qualify the annotation when importing the class. +import org.apache.flink.agents.api.skills.Skills; + +public class MathAgent extends Agent { + + @org.apache.flink.agents.api.annotation.Skills + public static Skills mySkills() { + // Load all skill subdirectories from a classpath resource (packaged in the jar). + return Skills.fromClasspath("skills"); + } +} +``` +{{< /tab >}} + +{{< /tabs >}} + +**Key points:** +- Use the decorator/annotation to declare a skill source. + - In Python, use `@skills`. + - In Java, use `@Skills`. +- Declare more than one `@skills`/`@Skills` method on the same agent to combine sources; the runtime merges them and de-duplicates identical entries. +- Declaring a skill source only makes the skills *available*. A skill is exposed to a chat model only when that model lists it in its `skills` (see [Enable Skills on a Chat Model](#enable-skills-on-a-chat-model)). + +### Skill Sources + +Each factory method creates a source with a different scheme: + +| Factory method (Python / Java) | Scheme | Description | +|--------------------------------|--------|-------------| +| `Skills.from_local_dir(*paths)` / `Skills.fromLocalDir(String...)` | `local` | One or more local directories, or `.zip` files, holding skill subdirectories. The path must be resolvable on the Flink TaskManager that runs the agent. | +| `Skills.from_url(*urls)` / `Skills.fromUrl(String...)` | `url` | One or more `http(s)` URLs, each pointing to a `.zip` whose top level holds the skill subdirectories. | +| `Skills.from_package(*pairs)` | `package` | **Python only.** One or more `(package, resource)` tuples locating skills inside an installed Python package. | +| `Skills.fromClasspath(String...)` | `classpath` | **Java only.** One or more classpath resource paths (e.g. under `src/main/resources/skills`). When packaged into a jar, the resource is materialized to a temp directory at runtime. | + +{{< hint info >}} +The `package` scheme is Python-only and the `classpath` scheme is Java-only. A plan written in one language using the other language's scheme deserializes fine, but fails fast at load time. Use `local` or `url` for cross-language skill sources. +{{< /hint >}} + +## Enable Skills on a Chat Model + +A declared skill becomes usable only when a chat model opts in by listing it in `skills`. When `skills` is set, the framework automatically: + +- injects the discovery prompt (the names and descriptions of the listed skills) into the system messages, and +- adds the two built-in tools the agent needs — `load_skill` (to read a skill's full instructions) and `bash` (to run its commands and scripts). + +{{< tabs "Enable Skills on a Chat Model" >}} + +{{< tab "Python" >}} +```python +@chat_model_setup +@staticmethod +def math_model() -> ResourceDescriptor: + return ResourceDescriptor( + clazz=ResourceName.ChatModel.OLLAMA_SETUP, + connection="ollama_server", + model="qwen3.5:9b", + prompt="system_prompt", + # Expose declared skills to this model by name. + skills=["math-calculator"], + # Whitelist the shell commands the bash tool is allowed to run. + allowed_commands=["echo", "bc"], + ) +``` +{{< /tab >}} + +{{< tab "Java" >}} +```java +@ChatModelSetup +public static ResourceDescriptor mathModel() { + return ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP) + .addInitialArgument("connection", "ollamaChatModelConnection") + .addInitialArgument("model", "qwen3.5:9b") + .addInitialArgument("prompt", "systemPrompt") + // Expose declared skills to this model by name. + .addInitialArgument("skills", List.of("math-calculator")) + // Whitelist the shell commands the bash tool is allowed to run. + .addInitialArgument("allowed_commands", List.of("echo", "bc")) + .build(); +} +``` +{{< /tab >}} + +{{< /tabs >}} + +**Key points:** +- `skills` lists the skill names (matching the `name` field in each `SKILL.md`) the agent may use. +- `allowed_commands` is a whitelist of shell command names the built-in `bash` tool may execute. Any command not on the list is rejected, so keep it as narrow as the skills require (for example `echo` and `bc` for arithmetic). +- The `load_skill` and `bash` tools are added automatically — you do not declare them in `tools`. They are added alongside any tools you do declare. +- Make sure the system prompt instructs the agent to load the relevant skill before acting, for example: *"You must load the skill first and strictly follow its instructions."* Without this nudge, smaller models may answer directly instead of consulting the skill. + +Skills work with both the [Workflow Agent]({{< ref "docs/development/workflow_agent" >}}) (configure the chat model via `@chat_model_setup`/`@ChatModelSetup` as above) and the [ReAct Agent]({{< ref "docs/development/react_agent" >}}) (set `skills` and `allowed_commands` on the `ReActAgent`'s chat model descriptor, and register the `Skills` resource on the execution environment with `add_resource(..., ResourceType.SKILLS, ...)`). + +## How Skills Work + +Once enabled, a request flows through the three progressive-disclosure stages: + +1. **Discovery.** The discovery prompt lists each available skill's `name` and `description` and explains how to load one. This is the only skill content the agent sees by default: + ```text + ## Available Skills + + + math-calculator + Calculate mathematical expressions using shell commands. Use when ... + + + ``` +2. **Activation.** When the agent decides a skill applies, it calls `load_skill(name="math-calculator")`. The framework returns the full `SKILL.md` body, including the skill's base directory and the absolute paths of its bundled resources. +3. **Execution.** Following the loaded instructions, the agent invokes `bash` to run commands (e.g. `echo "(2 + 3) * 4" | bc`) or bundled scripts (e.g. `python scripts/gen_joke.py`), and loads additional reference files only when an instruction points to them. + +This keeps each request lean — a skill the agent never activates costs only its one-line description. diff --git a/docs/content/docs/get-started/quickstart/skills_agent.md b/docs/content/docs/get-started/quickstart/skills_agent.md new file mode 100644 index 000000000..04eb0461c --- /dev/null +++ b/docs/content/docs/get-started/quickstart/skills_agent.md @@ -0,0 +1,360 @@ +--- +title: 'Skills' +weight: 3 +type: docs +--- + + +## Overview + +A [Skill]({{< ref "docs/development/skills" >}}) is a self-contained package of instructions, and optionally scripts and reference files, that teaches the agent how to perform a specialized task. Skills are loaded with *progressive disclosure*: only each skill's name and description are shown to the agent up front, and the full instructions are pulled in on demand when the agent decides a skill is relevant. + +This quickstart builds a small streaming agent that answers arithmetic questions. Rather than letting the LLM compute by itself, the agent exposes a `math-calculator` skill; for each question the agent loads the skill and follows its instructions to compute the result with the `bc` calculator through the built-in `bash` tool. It demonstrates the full skill lifecycle — discovery, activation, and execution — in a Flink streaming job. + +## Code Walkthrough + +### Define the Skill + +A skill is a directory containing a `SKILL.md` file with YAML frontmatter (loaded at discovery time) and a Markdown body (loaded on activation). Here is `skills/math-calculator/SKILL.md`: + +````markdown +--- +name: math-calculator +description: Calculate mathematical expressions using shell commands. Use when the user asks to perform arithmetic calculations like addition, subtraction, multiplication, division, or powers. +license: Apache-2.0 +compatibility: Requires bash with bc (basic calculator) +--- + +# Math Calculator Skill + +## When to Use +Use this skill when the user asks to evaluate a numeric expression. + +## Method +Pipe the expression into the `bc` (basic calculator) command: + +```bash +echo "(2 + 3) * 4" | bc +# Output: 20 +``` +```` + +### Create the Agent + +The agent declares where to load skills from with the `@skills`/`@Skills` decorator/annotation, and enables the skill on its chat model by listing it in `skills` together with the `allowed_commands` whitelist for the `bash` tool. For more details, please refer to the [Skills]({{< ref "docs/development/skills" >}}) documentation. + +{{< tabs "Create the Agent" >}} + +{{< tab "Python" >}} +```python +class MathAgent(Agent): + """An agent that answers arithmetic questions using the math-calculator skill.""" + + @skills + @staticmethod + def my_skills() -> Skills: + """Declare where to load skills from.""" + # Skills are bundled under this example package, loaded by package name. + return Skills.from_package( + ("flink_agents.examples.quickstart", "resources/skills") + ) + + @prompt + @staticmethod + def system_prompt() -> Prompt: + """System prompt instructing the agent to use the skill.""" + return Prompt.from_messages( + messages=[ + ChatMessage( + role=MessageRole.SYSTEM, + content="You are a helpful math assistant. Use the " + "math-calculator skill when asked to evaluate an expression. " + "You must load the skill first and strictly follow its " + "instructions. Reply with only the final numeric result.", + ) + ], + ) + + @chat_model_setup + @staticmethod + def math_model() -> ResourceDescriptor: + """ChatModel with the math-calculator skill enabled.""" + return ResourceDescriptor( + clazz=ResourceName.ChatModel.OLLAMA_SETUP, + connection="ollama_server", + model="qwen3:8b", + prompt="system_prompt", + # Expose the declared skill to this model by name. + skills=["math-calculator"], + # Whitelist the shell commands the built-in bash tool may run. + allowed_commands=["echo", "bc"], + ) + + @action(InputEvent.EVENT_TYPE) + @staticmethod + def process_input(event: Event, ctx: RunnerContext) -> None: + """Process input event and send a chat request to evaluate the question.""" + question: str = InputEvent.from_event(event).input + ctx.send_event( + ChatRequestEvent( + model="math_model", + messages=[ChatMessage(role=MessageRole.USER, content=question)], + ) + ) + + @action(ChatResponseEvent.EVENT_TYPE) + @staticmethod + def process_chat_response(event: Event, ctx: RunnerContext) -> None: + """Process chat response event and send the answer as output.""" + chat_response = ChatResponseEvent.from_event(event) + ctx.send_event(OutputEvent(output=chat_response.response.content)) +``` +{{< /tab >}} + +{{< tab "Java" >}} +```java +/** An agent that answers arithmetic questions using the math-calculator skill. */ +public class MathAgent extends Agent { + + /** Load skills from the skills/ directory packaged on the classpath. */ + @org.apache.flink.agents.api.annotation.Skills + public static Skills mySkills() { + return Skills.fromClasspath("skills"); + } + + /** System prompt instructing the agent to use the skill. */ + @Prompt + public static org.apache.flink.agents.api.prompt.Prompt systemPrompt() { + return org.apache.flink.agents.api.prompt.Prompt.fromMessages( + Collections.singletonList( + new ChatMessage( + MessageRole.SYSTEM, + "You are a helpful math assistant. Use the math-calculator skill " + + "when asked to evaluate an expression. You must load the " + + "skill first and strictly follow its instructions. Reply " + + "with only the final numeric result."))); + } + + /** ChatModel with the math-calculator skill enabled. */ + @ChatModelSetup + public static ResourceDescriptor mathModel() { + return ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP) + .addInitialArgument("connection", "ollamaChatModelConnection") + .addInitialArgument("model", "qwen3:8b") + .addInitialArgument("prompt", "systemPrompt") + // Expose the declared skill to this model by name. + .addInitialArgument("skills", List.of("math-calculator")) + // Whitelist the shell commands the built-in bash tool may run. + .addInitialArgument("allowed_commands", List.of("echo", "bc")) + .build(); + } + + /** Process input event and send a chat request to evaluate the question. */ + @Action(listenEventTypes = {InputEvent.EVENT_TYPE}) + public static void processInput(InputEvent event, RunnerContext ctx) { + ctx.sendEvent( + new ChatRequestEvent( + "mathModel", + Collections.singletonList( + new ChatMessage(MessageRole.USER, (String) event.getInput())))); + } + + /** Process chat response event and send the answer as output. */ + @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE}) + public static void processChatResponse(ChatResponseEvent event, RunnerContext ctx) { + ctx.sendEvent(new OutputEvent(event.getResponse().getContent())); + } +} +``` +{{< /tab >}} + +{{< /tabs >}} + +**Key points:** +- `@skills`/`@Skills` declares a skill source. `Skills.from_package` (Python) loads skills bundled inside an installed package by `(package, resource)`; `Skills.fromClasspath` (Java) loads them from a classpath resource packaged in the jar. +- A declared skill is exposed to a model only when the model lists it in `skills`. The `load_skill` and `bash` tools are then added automatically. +- `allowed_commands` whitelists the shell commands the `bash` tool may run — keep it as narrow as the skill requires. + +### Integrate the Agent with Flink + +Register the Ollama chat model connection, create a stream of questions, and apply the agent. + +{{< tabs "Integrate the Agent with Flink" >}} + +{{< tab "Python" >}} +```python +# Set up the Flink streaming environment and the Agents execution environment. +env = StreamExecutionEnvironment.get_execution_environment() +agents_env = AgentsExecutionEnvironment.get_execution_environment(env) + +# Add Ollama chat model connection to be used by the MathAgent. +agents_env.add_resource( + "ollama_server", ResourceType.CHAT_MODEL_CONNECTION, ollama_server_descriptor +) + +# A small stream of arithmetic questions to answer. +question_stream = env.from_collection( + ["What is (2 + 3) * 4?", "Compute 2 ^ 10.", "What is 144 divided by 12?"] +) + +# Use the MathAgent to answer each question with the math-calculator skill. +answer_stream = ( + agents_env.from_datastream(input=question_stream, key_selector=lambda x: x) + .apply(MathAgent()) + .to_datastream() +) + +# Print the answers to stdout, then execute the Flink pipeline. +answer_stream.print() +agents_env.execute() +``` +{{< /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 MathAgent. +agentsEnv.addResource( + "ollamaChatModelConnection", + ResourceType.CHAT_MODEL_CONNECTION, + CustomTypesAndResources.OLLAMA_SERVER_DESCRIPTOR); + +// A small stream of arithmetic questions to answer. +DataStream questionStream = + env.fromData("What is (2 + 3) * 4?", "Compute 2 ^ 10.", "What is 144 divided by 12?"); + +// Use the MathAgent to answer each question with the math-calculator skill. +DataStream answerStream = + agentsEnv.fromDataStream(questionStream).apply(new MathAgent()).toDataStream(); + +// Print the answers to stdout, then execute the Flink pipeline. +answerStream.print(); +agentsEnv.execute(); +``` +{{< /tab >}} + +{{< /tabs >}} + +## Run the Example + +### Prerequisites + +* Unix-like environment (we use Linux, Mac OS X, Cygwin, WSL) +* Git +* Java 11+ +* Python 3.10, 3.11 or 3.12 +* `bc` (basic calculator), used by the `math-calculator` skill — preinstalled on most Unix-like systems + +### 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 starting 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 steps. + +{{< 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:8b model, which is required by the quickstart examples. + +```bash +ollama pull qwen3:8b +``` + +### 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 the agent skills example +$FLINK_HOME/bin/flink run -py ./flink-agents/python/flink_agents/examples/quickstart/skills_agent_example.py +# or submit the example python file in installed flink-agents wheel +$FLINK_HOME/bin/flink run -py $PYTHONPATH/flink_agents/examples/quickstart/skills_agent_example.py +``` +{{< /tab >}} + +{{< tab "Java" >}} +```bash +$FLINK_HOME/bin/flink run -c org.apache.flink.agents.examples.SkillsAgentExample ./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/SkillsAgentExample.java b/examples/src/main/java/org/apache/flink/agents/examples/SkillsAgentExample.java new file mode 100644 index 000000000..8335dec03 --- /dev/null +++ b/examples/src/main/java/org/apache/flink/agents/examples/SkillsAgentExample.java @@ -0,0 +1,71 @@ +/* + * 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.agents.AgentExecutionOptions; +import org.apache.flink.agents.api.resource.ResourceType; +import org.apache.flink.agents.examples.agents.CustomTypesAndResources; +import org.apache.flink.agents.examples.agents.MathAgent; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; + +/** + * Java example demonstrating Agent Skills with Flink Agents. + * + *

A stream of arithmetic questions is processed by {@link MathAgent}, whose chat model has a + * {@code math-calculator} skill enabled. For each question the model loads the skill and follows + * its instructions to compute the answer with the {@code bc} calculator, then the result is printed + * to stdout. This serves as a minimal, end-to-end example of integrating skill-powered agents with + * Flink streaming jobs. + */ +public class SkillsAgentExample { + + /** 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); + + // limit async request to avoid overwhelming ollama server + agentsEnv.getConfig().set(AgentExecutionOptions.NUM_ASYNC_THREADS, 2); + + // Add Ollama chat model connection to be used by the MathAgent. + agentsEnv.addResource( + "ollamaChatModelConnection", + ResourceType.CHAT_MODEL_CONNECTION, + CustomTypesAndResources.OLLAMA_SERVER_DESCRIPTOR); + + // A small stream of arithmetic questions to answer. + DataStream questionStream = + env.fromData( + "What is (2 + 3) * 4?", "Compute 2 ^ 10.", "What is 144 divided by 12?"); + + // Use the MathAgent to answer each question with the math-calculator skill. + DataStream answerStream = + agentsEnv.fromDataStream(questionStream).apply(new MathAgent()).toDataStream(); + + // Print the answers to stdout. + answerStream.print(); + + // Execute the Flink pipeline. + agentsEnv.execute(); + } +} diff --git a/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java b/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java new file mode 100644 index 000000000..1056f877b --- /dev/null +++ b/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java @@ -0,0 +1,101 @@ +/* + * 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.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.annotation.Prompt; +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 org.apache.flink.agents.api.skills.Skills; + +import java.util.Collections; +import java.util.List; + +/** + * An agent that answers arithmetic questions using the {@code math-calculator} skill. + * + *

Instead of relying on the LLM to compute by itself, this agent exposes a {@code + * math-calculator} skill. When asked to evaluate an expression, the model loads the skill ({@code + * load_skill}) and follows its instructions to compute the result with the {@code bc} calculator + * through the built-in {@code bash} tool. + */ +public class MathAgent extends Agent { + + /** + * Load skills from the {@code skills/} directory packaged on the classpath. + * + *

The {@code @Skills} annotation and the {@link Skills} resource share a simple name but + * live in different packages, so the annotation is fully-qualified here. + */ + @org.apache.flink.agents.api.annotation.Skills + public static Skills mySkills() { + return Skills.fromClasspath("skills"); + } + + /** System prompt instructing the model to use the skill. */ + @Prompt + public static org.apache.flink.agents.api.prompt.Prompt systemPrompt() { + return org.apache.flink.agents.api.prompt.Prompt.fromMessages( + Collections.singletonList( + new ChatMessage( + MessageRole.SYSTEM, + "You are a helpful math assistant. Use the math-calculator skill " + + "when asked to evaluate an expression. You must load the " + + "skill first and strictly follow its instructions. Reply " + + "with only the final numeric result."))); + } + + /** ChatModel with the math-calculator skill enabled. */ + @ChatModelSetup + public static ResourceDescriptor mathModel() { + return ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP) + .addInitialArgument("connection", "ollamaChatModelConnection") + .addInitialArgument("model", "qwen3.5:9b") + .addInitialArgument("prompt", "systemPrompt") + // Expose the declared skill to this model by name. + .addInitialArgument("skills", List.of("math-calculator")) + // Whitelist the shell commands the built-in bash tool may run. + .addInitialArgument("allowed_commands", List.of("echo", "bc")) + .build(); + } + + /** Process input event and send a chat request to evaluate the question. */ + @Action(listenEventTypes = {InputEvent.EVENT_TYPE}) + public static void processInput(InputEvent event, RunnerContext ctx) { + ctx.sendEvent( + new ChatRequestEvent( + "mathModel", + Collections.singletonList( + new ChatMessage(MessageRole.USER, (String) event.getInput())))); + } + + /** Process chat response event and send the answer as output. */ + @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE}) + public static void processChatResponse(ChatResponseEvent event, RunnerContext ctx) { + ctx.sendEvent(new OutputEvent(event.getResponse().getContent())); + } +} diff --git a/examples/src/main/resources/skills/math-calculator/SKILL.md b/examples/src/main/resources/skills/math-calculator/SKILL.md new file mode 100644 index 000000000..c4f1071b5 --- /dev/null +++ b/examples/src/main/resources/skills/math-calculator/SKILL.md @@ -0,0 +1,43 @@ +--- +name: math-calculator +description: Calculate mathematical expressions using shell commands. Use when the user asks to perform arithmetic calculations like addition, subtraction, multiplication, division, or powers. +license: Apache-2.0 +compatibility: Requires bash with bc (basic calculator) +--- + +# Math Calculator Skill + +This skill calculates mathematical expressions using shell commands. + +## When to Use + +Use this skill when the user asks to evaluate a numeric expression, such as: + +- Basic arithmetic (add, subtract, multiply, divide) +- Expressions with parentheses +- Powers and square roots + +## Method + +Pipe the expression into the `bc` (basic calculator) command: + +```bash +echo "(2 + 3) * 4" | bc +# Output: 20 + +echo "2 ^ 10" | bc +# Output: 1024 +``` + +For decimal results, set the scale first: + +```bash +echo "scale=2; 10 / 3" | bc +# Output: 3.33 +``` + +## Instructions + +1. Translate the user's question into a single `bc` expression. +2. Run it with the `bash` tool: `echo "" | bc`. +3. Report the number printed by `bc` as the answer. diff --git a/python/flink_agents/examples/quickstart/agents/math_agent.py b/python/flink_agents/examples/quickstart/agents/math_agent.py new file mode 100644 index 000000000..20e3b8acf --- /dev/null +++ b/python/flink_agents/examples/quickstart/agents/math_agent.py @@ -0,0 +1,99 @@ +################################################################################ +# 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 flink_agents.api.agents.agent import Agent +from flink_agents.api.chat_message import ChatMessage, MessageRole +from flink_agents.api.decorators import action, chat_model_setup, prompt, skills +from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent +from flink_agents.api.events.event import Event, InputEvent, OutputEvent +from flink_agents.api.prompts.prompt import Prompt +from flink_agents.api.resource import ResourceDescriptor, ResourceName +from flink_agents.api.runner_context import RunnerContext +from flink_agents.api.skills import Skills + + +class MathAgent(Agent): + """An agent that answers arithmetic questions using the math-calculator skill. + + Instead of relying on the LLM to compute by itself, this agent exposes a + ``math-calculator`` skill. When asked to evaluate an expression, the model + loads the skill (``load_skill``) and follows its instructions to compute the + result with the ``bc`` calculator through the built-in ``bash`` tool. + """ + + @skills + @staticmethod + def my_skills() -> Skills: + """Declare where to load skills from. + + The skills are bundled under this example package's ``resources/skills`` + directory and loaded by package name, so the agent resolves them the + same way whether run from the source tree or the installed wheel. + """ + return Skills.from_package( + ("flink_agents.examples.quickstart", "resources/skills") + ) + + @prompt + @staticmethod + def system_prompt() -> Prompt: + """System prompt instructing the model to use the skill.""" + return Prompt.from_messages( + messages=[ + ChatMessage( + role=MessageRole.SYSTEM, + content="You are a helpful math assistant. Use the " + "math-calculator skill when asked to evaluate an expression. " + "You must load the skill first and strictly follow its " + "instructions. Reply with only the final numeric result.", + ) + ], + ) + + @chat_model_setup + @staticmethod + def math_model() -> ResourceDescriptor: + """ChatModel with the math-calculator skill enabled.""" + return ResourceDescriptor( + clazz=ResourceName.ChatModel.OLLAMA_SETUP, + connection="ollama_server", + model="qwen3.5:9b", + prompt="system_prompt", + # Expose the declared skill to this model by name. + skills=["math-calculator"], + # Whitelist the shell commands the built-in bash tool may run. + allowed_commands=["echo", "bc"], + ) + + @action(InputEvent.EVENT_TYPE) + @staticmethod + def process_input(event: Event, ctx: RunnerContext) -> None: + """Process input event and send a chat request to evaluate the question.""" + question: str = InputEvent.from_event(event).input + ctx.send_event( + ChatRequestEvent( + model="math_model", + messages=[ChatMessage(role=MessageRole.USER, content=question)], + ) + ) + + @action(ChatResponseEvent.EVENT_TYPE) + @staticmethod + def process_chat_response(event: Event, ctx: RunnerContext) -> None: + """Process chat response event and send the answer as output.""" + chat_response = ChatResponseEvent.from_event(event) + ctx.send_event(OutputEvent(output=chat_response.response.content)) diff --git a/python/flink_agents/examples/quickstart/resources/skills/math-calculator/SKILL.md b/python/flink_agents/examples/quickstart/resources/skills/math-calculator/SKILL.md new file mode 100644 index 000000000..c4f1071b5 --- /dev/null +++ b/python/flink_agents/examples/quickstart/resources/skills/math-calculator/SKILL.md @@ -0,0 +1,43 @@ +--- +name: math-calculator +description: Calculate mathematical expressions using shell commands. Use when the user asks to perform arithmetic calculations like addition, subtraction, multiplication, division, or powers. +license: Apache-2.0 +compatibility: Requires bash with bc (basic calculator) +--- + +# Math Calculator Skill + +This skill calculates mathematical expressions using shell commands. + +## When to Use + +Use this skill when the user asks to evaluate a numeric expression, such as: + +- Basic arithmetic (add, subtract, multiply, divide) +- Expressions with parentheses +- Powers and square roots + +## Method + +Pipe the expression into the `bc` (basic calculator) command: + +```bash +echo "(2 + 3) * 4" | bc +# Output: 20 + +echo "2 ^ 10" | bc +# Output: 1024 +``` + +For decimal results, set the scale first: + +```bash +echo "scale=2; 10 / 3" | bc +# Output: 3.33 +``` + +## Instructions + +1. Translate the user's question into a single `bc` expression. +2. Run it with the `bash` tool: `echo "" | bc`. +3. Report the number printed by `bc` as the answer. diff --git a/python/flink_agents/examples/quickstart/skills_agent_example.py b/python/flink_agents/examples/quickstart/skills_agent_example.py new file mode 100644 index 000000000..c9dd8f161 --- /dev/null +++ b/python/flink_agents/examples/quickstart/skills_agent_example.py @@ -0,0 +1,77 @@ +################################################################################ +# 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.core_options import AgentExecutionOptions +from flink_agents.api.execution_environment import AgentsExecutionEnvironment +from flink_agents.api.resource import ResourceType +from flink_agents.examples.quickstart.agents.custom_types_and_resources import ( + ollama_server_descriptor, +) +from flink_agents.examples.quickstart.agents.math_agent import MathAgent + + +def main() -> None: + """Main function for the agent skills quickstart example. + + This example demonstrates how to use Agent Skills with Flink Agents. A stream + of arithmetic questions is processed by ``MathAgent``, whose chat model has a + ``math-calculator`` skill enabled. For each question the model loads the skill + and follows its instructions to compute the answer with the ``bc`` calculator, + then the result is printed to stdout. This serves as a minimal, end-to-end + example of integrating skill-powered agents with Flink streaming jobs. + """ + # Set up the Flink streaming environment and the Agents execution environment. + env = StreamExecutionEnvironment.get_execution_environment() + agents_env = AgentsExecutionEnvironment.get_execution_environment(env) + + # limit async request to avoid overwhelming ollama server + agents_env.get_config().set(AgentExecutionOptions.NUM_ASYNC_THREADS, 2) + + # Add Ollama chat model connection to be used by the MathAgent. + agents_env.add_resource( + "ollama_server", + ResourceType.CHAT_MODEL_CONNECTION, + ollama_server_descriptor, + ) + + # A small stream of arithmetic questions to answer. + question_stream = env.from_collection( + [ + "What is (2 + 3) * 4?", + "Compute 2 ^ 10.", + "What is 144 divided by 12?", + ], + ) + + # Use the MathAgent to answer each question with the math-calculator skill. + answer_stream = ( + agents_env.from_datastream(input=question_stream, key_selector=lambda x: x) + .apply(MathAgent()) + .to_datastream() + ) + + # Print the answers to stdout. + answer_stream.print() + + # Execute the Flink pipeline. + agents_env.execute() + + +if __name__ == "__main__": + main() diff --git a/python/pyproject.toml b/python/pyproject.toml index 0c8f74e5d..f5d9814c5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -71,7 +71,7 @@ exclude = ["_build_backend*"] [tool.setuptools.package-data] "flink_agents.lib" = ["**/*.jar"] "flink_agents.examples.quickstart" = ["**/*.yaml"] -"flink_agents.examples.quickstart.resources" = ["**/*.txt"] +"flink_agents.examples.quickstart.resources" = ["**/*.txt", "**/*.md"] # Optional dependencies (dependency groups) [project.optional-dependencies]