Skip to content
Merged
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
6 changes: 0 additions & 6 deletions agentscope-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@
</exclusions>
</dependency>

<!-- OpenAI Java SDK -->
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
</dependency>

<!-- Google Gemini Java SDK -->
<dependency>
<groupId>com.google.genai</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,15 +523,21 @@ private void cleanupStructuredOutputHistory(Msg finalResponseMsg) {
assistantMsgIndex,
toolMsgIndex);

// Delete from higher index first to avoid index shifting issues
memory.deleteMessage(toolMsgIndex);
memory.deleteMessage(assistantMsgIndex);
// Remove all messages from the first generate_response call to the end
// This handles cases where the model retried multiple times, leaving multiple
// intermediate ASSISTANT/TOOL message pairs in memory
int messagesToDelete = currentSize - assistantMsgIndex;
for (int i = 0; i < messagesToDelete; i++) {
memory.deleteMessage(
assistantMsgIndex); // Always delete at same index after each removal
}

memory.addMessage(finalResponseMsg);

log.debug(
"Cleanup complete. Memory now has {} messages (was {})",
"Cleanup complete. Memory now has {} messages (was {}, deleted {})",
memory.getMessages().size(),
currentSize);
currentSize,
messagesToDelete);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ private String determineKey(ToolUseBlock block) {
// Remember this key if it's not a placeholder
if (block.getName() != null && !isPlaceholder(block.getName())) {
lastToolCallKey = key;
return key;
}
return key;
}

// 2. Use tool name (non-placeholder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ void applyOptions(
*/
void applyTools(TParams paramsBuilder, List<ToolSchema> tools);

/**
* Apply tool schemas to provider-specific request parameters with provider compatibility handling.
*
* <p>This method allows formatters to detect the provider from baseUrl/modelName and adjust
* tool definitions for compatibility (e.g., removing unsupported parameters like 'strict').
*
* <p>The default implementation delegates to {@code applyTools(TParams, List)}.
* Formatters that support provider-specific tool handling should override this method.
*
* @param paramsBuilder Provider-specific request parameters builder
* @param tools Tool schemas to apply (may be null or empty)
* @param baseUrl API base URL for provider detection (null for default)
* @param modelName Model name for provider detection fallback (null)
*/
default void applyTools(
TParams paramsBuilder, List<ToolSchema> tools, String baseUrl, String modelName) {
// Default implementation: delegate to the simpler method
applyTools(paramsBuilder, tools);
}

/**
* Apply tool choice configuration to provider-specific request parameters.
*
Expand All @@ -92,4 +112,24 @@ default void applyToolChoice(TParams paramsBuilder, ToolChoice toolChoice) {
// Default implementation: do nothing
// Subclasses can override to provide provider-specific behavior
}

/**
* Apply tool choice configuration to provider-specific request parameters with provider detection.
*
* <p>This method allows formatters to detect the provider from baseUrl/modelName and adjust
* tool_choice format or gracefully degrade when the provider doesn't support certain options.
*
* <p>The default implementation delegates to {@code applyToolChoice(TParams, ToolChoice)}.
* Formatters that support provider-specific tool_choice handling should override this method.
*
* @param paramsBuilder Provider-specific request parameters builder
* @param toolChoice Tool choice configuration (null means provider default)
* @param baseUrl API base URL for provider detection (null for default)
* @param modelName Model name for provider detection fallback (null)
*/
default void applyToolChoice(
TParams paramsBuilder, ToolChoice toolChoice, String baseUrl, String modelName) {
// Default implementation: delegate to the simpler method
applyToolChoice(paramsBuilder, toolChoice);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.agentscope.core.formatter;

import com.openai.models.chat.completions.ChatCompletionContentPartInputAudio;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -86,8 +85,15 @@ public static boolean isLocalFile(String url) {
* @throws IOException If file cannot be read or exceeds size limit
*/
public static String fileToBase64(String path) throws IOException {
Path filePath = Path.of(path);
if (!Files.exists(filePath)) {
throw new IOException("File does not exist: " + path);
}
if (!Files.isReadable(filePath)) {
throw new IOException("File is not readable: " + path);
}
checkFileSize(path);
byte[] bytes = Files.readAllBytes(Path.of(path));
byte[] bytes = Files.readAllBytes(filePath);
return Base64.getEncoder().encodeToString(bytes);
}

Expand All @@ -104,33 +110,36 @@ public static String downloadUrlToBase64(String url) throws IOException {
log.debug("Downloading remote URL for base64 encoding: {}", url);

HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000); // 10 seconds
connection.setReadTimeout(30000); // 30 seconds
connection.connect();

int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("Failed to download URL: HTTP " + responseCode + " for " + url);
}

try (InputStream is = connection.getInputStream()) {
byte[] bytes = is.readAllBytes();
try {
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000); // 10 seconds
connection.setReadTimeout(30000); // 30 seconds
connection.connect();

// Check size after download
if (bytes.length > MAX_SIZE_BYTES) {
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException(
"Downloaded content too large: "
+ bytes.length
+ " bytes (max: "
+ MAX_SIZE_BYTES
+ ")");
}
if (bytes.length > WARN_SIZE_BYTES) {
log.warn("Large download detected: {} bytes from {}", bytes.length, url);
"Failed to download URL: HTTP " + responseCode + " for " + url);
}

return Base64.getEncoder().encodeToString(bytes);
try (InputStream is = connection.getInputStream()) {
byte[] bytes = is.readAllBytes();

// Check size after download
if (bytes.length > MAX_SIZE_BYTES) {
throw new IOException(
"Downloaded content too large: "
+ bytes.length
+ " bytes (max: "
+ MAX_SIZE_BYTES
+ ")");
}
if (bytes.length > WARN_SIZE_BYTES) {
log.warn("Large download detected: {} bytes from {}", bytes.length, url);
}

return Base64.getEncoder().encodeToString(bytes);
}
} finally {
connection.disconnect();
}
Expand Down Expand Up @@ -306,25 +315,34 @@ public static void validateVideoExtension(String url) {
}

/**
* Determine OpenAI audio format from file extension.
* Determine audio format string from file extension.
*
* @param path The file path
* @return Audio format string ("wav" or "mp3")
*/
public static ChatCompletionContentPartInputAudio.InputAudio.Format determineAudioFormat(
String path) {
public static String determineAudioFormat(String path) {
String ext = getExtension(path).toLowerCase();
return ext.equals("wav")
? ChatCompletionContentPartInputAudio.InputAudio.Format.WAV
: ChatCompletionContentPartInputAudio.InputAudio.Format.MP3;
return ext.equals("wav") ? "wav" : "mp3";
}

/**
* Infer OpenAI audio format from MIME type.
* Infer audio format string from MIME type.
*
* @param mediaType The MIME type
* @return Audio format string ("wav", "mp3", "opus", "flac", etc.)
*/
public static ChatCompletionContentPartInputAudio.InputAudio.Format
inferAudioFormatFromMediaType(String mediaType) {
if (mediaType != null && mediaType.contains("wav")) {
return ChatCompletionContentPartInputAudio.InputAudio.Format.WAV;
public static String inferAudioFormatFromMediaType(String mediaType) {
if (mediaType == null) {
return "mp3"; // default
}
if (mediaType.contains("wav")) {
return "wav";
} else if (mediaType.contains("opus")) {
return "opus";
} else if (mediaType.contains("flac")) {
return "flac";
}
return ChatCompletionContentPartInputAudio.InputAudio.Format.MP3; // default
return "mp3"; // default
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed 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 io.agentscope.core.formatter.openai;

import io.agentscope.core.formatter.AbstractBaseFormatter;
import io.agentscope.core.formatter.openai.dto.OpenAIMessage;
import io.agentscope.core.formatter.openai.dto.OpenAIRequest;
import io.agentscope.core.formatter.openai.dto.OpenAIResponse;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.ToolChoice;
import io.agentscope.core.model.ToolSchema;
import java.time.Instant;
import java.util.List;

/**
* Base formatter for OpenAI Chat Completion HTTP API.
* Provides common functionality for both single-agent and multi-agent formatters.
*/
public abstract class OpenAIBaseFormatter
extends AbstractBaseFormatter<OpenAIMessage, OpenAIResponse, OpenAIRequest> {

protected final OpenAIMessageConverter messageConverter;
protected final OpenAIResponseParser responseParser;
protected final OpenAIToolsHelper toolsHelper;

protected OpenAIBaseFormatter() {
this.messageConverter =
new OpenAIMessageConverter(
this::extractTextContent, this::convertToolResultToString);
this.responseParser = new OpenAIResponseParser();
this.toolsHelper = new OpenAIToolsHelper();
}

@Override
public ChatResponse parseResponse(OpenAIResponse response, Instant startTime) {
return responseParser.parseResponse(response, startTime);
}

@Override
public void applyOptions(
OpenAIRequest request, GenerateOptions options, GenerateOptions defaultOptions) {
toolsHelper.applyOptions(request, options, defaultOptions);
}

@Override
public void applyTools(OpenAIRequest request, List<ToolSchema> tools) {
toolsHelper.applyTools(request, tools);
}

@Override
public void applyTools(
OpenAIRequest request, List<ToolSchema> tools, String baseUrl, String modelName) {
ProviderCapability capability = ProviderCapability.fromUrl(baseUrl);
if (capability == ProviderCapability.UNKNOWN && modelName != null) {
capability = ProviderCapability.fromModelName(modelName);
}
toolsHelper.applyTools(request, tools, capability);
}

@Override
public void applyToolChoice(OpenAIRequest request, ToolChoice toolChoice) {
toolsHelper.applyToolChoice(request, toolChoice);
}

@Override
public void applyToolChoice(
OpenAIRequest request, ToolChoice toolChoice, String baseUrl, String modelName) {
toolsHelper.applyToolChoice(request, toolChoice, baseUrl, modelName);
}

/**
* Build a basic OpenAIRequest.
*
* @param model Model name
* @param messages Formatted OpenAI messages
* @param stream Whether to enable streaming
* @return Basic OpenAIRequest
*/
public OpenAIRequest buildRequest(String model, List<OpenAIMessage> messages, boolean stream) {
return OpenAIRequest.builder().model(model).messages(messages).stream(stream).build();
}

/**
* Build a complete OpenAIRequest with full configuration.
* This method is provided for convenience but usage via the standard Formatter interface
* (instantiating request manually and calling apply methods) is preferred in generic code.
*
* @param model Model name
* @param messages Formatted OpenAI messages
* @param stream Whether to enable streaming
* @param options Generation options
* @param defaultOptions Default generation options
* @param tools Tool schemas
* @param toolChoice Tool choice configuration
* @return Complete OpenAIRequest ready for API call
*/
public OpenAIRequest buildRequest(
String model,
List<OpenAIMessage> messages,
boolean stream,
GenerateOptions options,
GenerateOptions defaultOptions,
List<ToolSchema> tools,
ToolChoice toolChoice) {

OpenAIRequest request =
OpenAIRequest.builder().model(model).messages(messages).stream(stream).build();

applyOptions(request, options, defaultOptions);
applyTools(request, tools);

if (toolChoice != null) {
applyToolChoice(request, toolChoice);
}

return request;
}
}
Loading
Loading