Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.flink.agents.api.resource.ResourceDescriptor;
import org.apache.flink.agents.api.resource.ResourceType;
import org.apache.flink.agents.api.skills.Skills;
import org.apache.flink.agents.api.subagent.SubagentSetup;
import org.apache.flink.agents.api.tools.Tool;
import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.util.Preconditions;
Expand All @@ -36,14 +37,18 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public abstract class BaseChatModelSetup extends Resource {

protected final String connectionName;
protected String model;
protected Object prompt;
protected List<String> toolNames;
protected final List<String> subagentNames;
@Nullable protected List<String> skills;
@Nullable protected String skillDiscoveryPrompt;
protected List<String> allowedCommands;
Expand All @@ -59,6 +64,9 @@ public BaseChatModelSetup(ResourceDescriptor descriptor, ResourceContext resourc
this.model = descriptor.getArgument("model");
this.prompt = descriptor.getArgument("prompt");
this.toolNames = descriptor.getArgument("tools");
List<String> declaredSubagents = descriptor.getArgument("subagents");
this.subagentNames =
declaredSubagents == null ? new ArrayList<>() : new ArrayList<>(declaredSubagents);
this.skills = descriptor.getArgument("skills");
List<String> declaredCommands = descriptor.getArgument("allowed_commands");
this.allowedCommands =
Expand Down Expand Up @@ -93,7 +101,7 @@ public void open() throws Exception {
}
if (this.skills != null) {
this.skillDiscoveryPrompt =
this.resourceContext.generateAvailableSkillsPrompt(this.skills);
nullIfEmpty(this.resourceContext.generateAvailableSkillsPrompt(this.skills));
List<String> mutable =
this.toolNames == null ? new ArrayList<>() : new ArrayList<>(this.toolNames);
if (!mutable.contains(Skills.LOAD_SKILL_TOOL)) {
Expand All @@ -104,11 +112,52 @@ public void open() throws Exception {
}
this.toolNames = mutable;
}
// Rebuilt from scratch: open() may run again on the same instance, and the callables must
// not accumulate.
this.tools.clear();
Set<String> callableNames = new LinkedHashSet<>();
if (this.toolNames != null) {
for (String name : this.toolNames) {
Preconditions.checkState(
callableNames.add(name), "Duplicate callable name: %s", name);
this.tools.add((Tool) this.resourceContext.getResource(name, ResourceType.TOOL));
}
}
for (String name : this.subagentNames) {
// Tools are forbidden to carry the reserved prefix at registration, so a prefixed
// callable name can only come from this loop and a clash with a tool is impossible.
// Checked before the schema below, because a name declared twice is a mistake in the
// declaration whether or not it ends up registered.
Preconditions.checkState(
callableNames.add(SubagentSetup.CALLABLE_NAME_PREFIX + name),
"Duplicate callable name: %s",
SubagentSetup.CALLABLE_NAME_PREFIX + name);
Resource resource = this.resourceContext.getResource(name, ResourceType.AGENT);
// A sub-agent owned by the other language resolves to a bridge handle here, which
// carries no schema to declare, so it is rejected instead of silently dropped.
Preconditions.checkState(
resource instanceof SubagentSetup,
"Sub-agent %s must resolve to a SubagentSetup, but was %s",
name,
resource.getClass().getName());
SubagentSetup setup = (SubagentSetup) resource;
String inputSchema = setup.getInputSchema();
// A sub-agent that declares neither an input schema nor an input type gives the model
// no arguments to build a call from, so the declaration is a mistake rather than
// something to skip: fail the job at setup time, consistent with the duplicate-name and
// bridge-handle checks above.
Preconditions.checkState(
inputSchema != null,
"Sub-agent %s declares neither an input schema nor an input type, so there are"
+ " no arguments for the model to build a call from.",
name);
this.tools.add(new SubagentTool(name, setup.getDescription(), inputSchema));
}
}

@Nullable
private static String nullIfEmpty(@Nullable String value) {
return value == null || value.isEmpty() ? null : value;
}

public abstract Map<String, Object> getParameters();
Expand Down Expand Up @@ -170,10 +219,11 @@ public List<ChatMessage> prepareRequestMessages(
messages = promptMessages;
}

if (this.skillDiscoveryPrompt != null && !this.skillDiscoveryPrompt.isEmpty()) {
int idx = ChatMessage.findFirstSystemMessage(messages);
if (this.skillDiscoveryPrompt != null) {
// Right after the first system message, or at the head when there is none.
int idx = ChatMessage.findFirstSystemMessage(messages) + 1;
List<ChatMessage> mutated = new ArrayList<>(messages);
mutated.add(idx + 1, new ChatMessage(MessageRole.SYSTEM, this.skillDiscoveryPrompt));
mutated.add(idx, new ChatMessage(MessageRole.SYSTEM, this.skillDiscoveryPrompt));
messages = mutated;
}
return messages;
Expand Down Expand Up @@ -225,6 +275,16 @@ public List<String> getToolNames() {
return toolNames;
}

/** Names of the {@code AGENT} resources this setup declares as delegable. */
public List<String> getSubagentNames() {
return subagentNames;
}

@VisibleForTesting
public List<Tool> getTools() {
return tools;
}

@Nullable
public List<String> getSkills() {
return skills;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.api.chat.model;

import org.apache.flink.agents.api.subagent.SubagentSetup;
import org.apache.flink.agents.api.tools.Tool;
import org.apache.flink.agents.api.tools.ToolMetadata;
import org.apache.flink.agents.api.tools.ToolParameters;
import org.apache.flink.agents.api.tools.ToolResponse;
import org.apache.flink.agents.api.tools.ToolType;

/**
* Presents an {@code AGENT} resource to a chat model as a callable, so that the model can delegate
* a task by issuing a function call. The callable name carries the reserved {@link
* SubagentSetup#CALLABLE_NAME_PREFIX}, which is how the executing side tells a delegation apart
* from a plain tool call.
*
* <p>Metadata only: it carries the schema the model needs to build the call, and nothing else. The
* call itself is dispatched by resolving the {@code AGENT} resource at execution time, so {@link
* #call} is never reached.
*/
class SubagentTool extends Tool {

SubagentTool(String agentName, String description, String inputSchema) {
super(
new ToolMetadata(
SubagentSetup.CALLABLE_NAME_PREFIX + agentName,
effectiveDescription(agentName, description),
inputSchema));
}

/**
* Falls back to a generic delegation description, so an undescribed sub-agent stays usable.
* Every description ends with the sub-agent marker, which replaces a separate listing message:
* the model learns that the callable is a delegation from the description alone.
*/
private static String effectiveDescription(String agentName, String description) {
String effective =
description == null || description.isBlank()
? "Delegate a standalone task to sub-agent " + agentName
: description;
return effective + " This is subagent.";
}

/**
* Sub-agents are declared to the model as plain functions: the model builds the call the same
* way it builds a tool call, and only the executing side tells them apart.
*/
@Override
public ToolType getToolType() {
return ToolType.FUNCTION;
}

@Override
public ToolResponse call(ToolParameters parameters) {
throw new UnsupportedOperationException(
"SubagentTool is metadata-only; resolve the AGENT resource at execution time.");
}
}
Loading
Loading