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
24 changes: 22 additions & 2 deletions agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,17 @@ private Mono<Msg> doStructuredCall(List<Msg> msgs, Class<?> targetClass, JsonNod
? model.supportsNativeStructuredOutputWithTools()
: model.supportsNativeStructuredOutput();
if (useNative) {
return doNativeStructuredCall(msgs, jsonSchema);
return doNativeStructuredCall(msgs, jsonSchema)
.onErrorResume(
e -> {
log.warn(
"Native structured output failed ({}) — falling back to"
+ " synthetic tool path",
e.getMessage() != null
? e.getMessage()
: e.getClass().getSimpleName());
return doFallbackStructuredCall(msgs, jsonSchema);
});
}
return doFallbackStructuredCall(msgs, jsonSchema);
}
Expand Down Expand Up @@ -1075,11 +1085,21 @@ private Mono<Msg> doNativeStructuredCall(List<Msg> msgs, Map<String, Object> jso
.strict(true)
.build());

int contextSizeBefore = scope.state.contextMutable().size();

return scope.doCallInner(msgs)
.flatMap(
result -> {
Msg out = wrapNativeStructuredResult(result);
return saveStateToSession(scope).thenReturn(out);
})
.doOnError(
e -> {
List<Msg> ctx = scope.state.contextMutable();
while (ctx.size() > contextSizeBefore) {
ctx.remove(ctx.size() - 1);
}
scope.nativeResponseFormat = null;
});
});
}
Expand Down Expand Up @@ -1912,7 +1932,7 @@ private Mono<Msg> reasoning(int iter, boolean ignoreMaxIters) {
event.getEffectiveGenerateOptions() != null
? event.getEffectiveGenerateOptions()
: buildGenerateOptions();
if (nativeResponseFormat != null) {
if (nativeResponseFormat != null && soTool == null) {
options =
GenerateOptions.mergeOptions(
GenerateOptions.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.agentscope.core.formatter.dashscope;

import io.agentscope.core.formatter.ResponseFormat;
import io.agentscope.core.formatter.dashscope.dto.DashScopeFunction;
import io.agentscope.core.formatter.dashscope.dto.DashScopeParameters;
import io.agentscope.core.formatter.dashscope.dto.DashScopeTool;
Expand Down Expand Up @@ -103,6 +104,12 @@ public void applyOptions(
if (parallelToolCalls != null) {
params.setParallelToolCalls(parallelToolCalls);
}

ResponseFormat responseFormat =
getOption(options, defaultOptions, GenerateOptions::getResponseFormat);
if (responseFormat != null) {
params.setResponseFormat(responseFormat);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class DashScopeChatModel extends ChatModelBase {
private final boolean stream;
private final Boolean enableThinking; // nullable
private final Boolean enableSearch; // nullable
private Boolean nativeStructuredOutput; // nullable, set by Builder
private final EndpointType endpointType;
private final GenerateOptions defaultOptions;
private final Formatter<DashScopeMessage, DashScopeResponse, DashScopeRequest> formatter;
Expand Down Expand Up @@ -152,6 +153,7 @@ public DashScopeChatModel(
this.stream = enableThinking != null && enableThinking ? true : stream;
this.enableThinking = enableThinking;
this.enableSearch = enableSearch;
this.nativeStructuredOutput = null;
this.endpointType = endpointType != null ? endpointType : EndpointType.AUTO;
this.defaultOptions =
defaultOptions != null ? defaultOptions : GenerateOptions.builder().build();
Expand Down Expand Up @@ -384,7 +386,13 @@ public String getModelName() {

@Override
public boolean supportsNativeStructuredOutput() {
return true;
if (Boolean.TRUE.equals(enableThinking)) {
return false;
}
if (nativeStructuredOutput != null) {
return nativeStructuredOutput;
}
return false;
}

public static class Builder {
Expand All @@ -401,6 +409,7 @@ public static class Builder {
private boolean enableEncrypt = false;
private ProxyConfig proxyConfig;
private int contextWindowSize = -1;
private Boolean nativeStructuredOutput;
private Boolean nativeStructuredOutputWithTools;

/**
Expand Down Expand Up @@ -631,13 +640,33 @@ public Builder contextWindowSize(int contextWindowSize) {
return this;
}

/**
* Sets whether this model supports native structured output via {@code response_format}
* with {@code json_schema} type.
*
* <p>Defaults to {@code false}. DashScope's native endpoint only supports
* {@code json_object} (free-form JSON), not {@code json_schema} (strict schema
* validation). When {@code false}, the framework uses the {@code generate_response}
* tool fallback for structured output requests.
*
* <p>Set to {@code true} only if your model/endpoint is confirmed to support
* {@code json_schema} in {@code response_format}.
*
* @param nativeStructuredOutput true to enable native json_schema path
* @return this builder instance
*/
public Builder nativeStructuredOutput(boolean nativeStructuredOutput) {
this.nativeStructuredOutput = nativeStructuredOutput;
return this;
}

/**
* Sets whether this model correctly handles native structured output
* ({@code response_format}) alongside tool calling.
*
* <p>Defaults to {@code true}, which is correct for Qwen models on DashScope.
* Set to {@code false} for third-party models hosted on DashScope that
* prioritise {@code response_format} over tool invocations.
* <p>Defaults to {@code false} (inherits from
* {@link #nativeStructuredOutput(boolean)}). Set to {@code true} only for models
* that support both {@code response_format} and tool calling simultaneously.
*
* @param nativeStructuredOutputWithTools false to use fallback when tools are present
* @return this builder instance
Expand Down Expand Up @@ -699,6 +728,9 @@ public DashScopeChatModel build() {
contextWindowSize >= 0
? contextWindowSize
: ModelContextWindows.lookup(modelName, ModelContextWindows.DASHSCOPE));
if (nativeStructuredOutput != null) {
model.nativeStructuredOutput = nativeStructuredOutput;
}
if (nativeStructuredOutputWithTools != null) {
model.setNativeStructuredOutputWithTools(nativeStructuredOutputWithTools);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,22 @@ protected ChatResponse parseCompletionResponse(OpenAIResponse response, Instant

Map<String, Object> argsMap = new HashMap<>();
if (!arguments.isEmpty()) {
@SuppressWarnings("unchecked")
Map<String, Object> parsed =
JsonUtils.getJsonCodec()
.fromJson(arguments, Map.class);
if (parsed != null) {
argsMap.putAll(parsed);
try {
@SuppressWarnings("unchecked")
Map<String, Object> parsed =
JsonUtils.getJsonCodec()
.fromJson(arguments, Map.class);
if (parsed != null) {
argsMap.putAll(parsed);
}
} catch (Exception parseEx) {
log.warn(
"Failed to parse tool call arguments as JSON;"
+ " preserving raw arguments: id={},"
+ " name={}, error={}",
toolCallId,
name,
parseEx.getMessage());
}
}

Expand Down
62 changes: 48 additions & 14 deletions docs/v2/en/docs/building-blocks/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,54 @@ WeatherInfo info = msg.getStructuredData(WeatherInfo.class);

How it works: the framework synthesizes a forced structured tool call from the target class, validates and repairs the model output, and writes the result into `Msg.metadata` under the `structured_output` key, so `getStructuredData(Class)` can deserialize it directly. Complete example: `agentscope-examples/documentation/.../structuredoutput/StructuredOutputExample.java`.

> **Structured output with tool calling**
>
> When an agent has both tools and structured output, some OpenAI-compatible providers (e.g. Kimi, Deepseek) prioritise the `response_format` constraint and skip tool calling entirely. If you encounter this, set `nativeStructuredOutputWithTools(false)` when building the model — the framework will use a synthetic tool approach for structured output, fully compatible with the ReAct tool-calling loop:
>
> ```java
> OpenAIChatModel model = OpenAIChatModel.builder()
> .apiKey("...")
> .baseUrl("https://api.moonshot.cn/v1")
> .modelName("moonshot-v1-8k")
> .nativeStructuredOutputWithTools(false)
> .build();
> ```
>
> `DashScopeChatModel` supports this option as well. For native OpenAI models (GPT-4o, etc.) the default behavior handles both correctly — no configuration needed.
#### Structured output path selection

The framework provides two structured output paths:

| Path | Condition | Mechanism |
|------|-----------|-----------|
| **Native** | `supportsNativeStructuredOutput() = true` | Uses `response_format` + `json_schema` for direct JSON output |
| **Fallback** (default) | `supportsNativeStructuredOutput() = false` | Injects a `generate_response` synthetic tool; model returns structured data via tool call |

If the native path fails (e.g. model returns HTTP 400), the framework **automatically falls back** to the synthetic tool path — no user intervention needed.

#### Default behavior per provider

| Provider | `supportsNativeStructuredOutput` | Notes |
|----------|----------------------------------|-------|
| OpenAI (GPT-4o, etc.) | `true` | Native `json_schema` support |
| OpenAI (DeepSeek/GLM formatter) | `false` | Not supported; auto-fallback |
| DashScope | `false` | Native endpoint only supports `json_object`, not `json_schema`; fallback by default |
| Anthropic | `false` (default) | — |

> **DashScope users**: Thinking mode (`enableThinking(true)`) does not support structured output at all — the framework forces the fallback path.

#### Explicit configuration

If you confirm your model/endpoint supports `json_schema`, enable the native path via builder:

```java
DashScopeChatModel model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.nativeStructuredOutput(true) // explicitly enable native json_schema path
.build();
```

#### Structured output with tool calling

When an agent has both tools and structured output, some OpenAI-compatible providers (e.g. Kimi, Deepseek) prioritise the `response_format` constraint and skip tool calling entirely. Set `nativeStructuredOutputWithTools(false)` to resolve this:

```java
OpenAIChatModel model = OpenAIChatModel.builder()
.apiKey("...")
.baseUrl("https://api.moonshot.cn/v1")
.modelName("moonshot-v1-8k")
.nativeStructuredOutputWithTools(false)
.build();
```

`DashScopeChatModel` supports this option as well. For native OpenAI models (GPT-4o, etc.) the default behavior handles both correctly — no configuration needed.

### Formatter

Expand Down
62 changes: 48 additions & 14 deletions docs/v2/zh/docs/building-blocks/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,54 @@ WeatherInfo info = msg.getStructuredData(WeatherInfo.class);

实现细节:框架会基于目标 Class 合成强制结构化的工具调用,再校验并修复模型输出,最后把结果挂到 `Msg.metadata` 的 `structured_output` 字段,供 `getStructuredData(Class)` 直接反序列化。完整示例:`agentscope-examples/documentation/.../structuredoutput/StructuredOutputExample.java`。

> **结构化输出与工具调用共存**
>
> 当 Agent 同时注册了工具并请求结构化输出时,部分 OpenAI 兼容 API(如 Kimi、Deepseek 等)会优先遵循 `response_format` 约束而跳过工具调用。如果遇到此问题,在构建 Model 时设置 `nativeStructuredOutputWithTools(false)`,框架将改用合成工具方式输出结构化结果,与工具调用完全兼容:
>
> ```java
> OpenAIChatModel model = OpenAIChatModel.builder()
> .apiKey("...")
> .baseUrl("https://api.moonshot.cn/v1")
> .modelName("moonshot-v1-8k")
> .nativeStructuredOutputWithTools(false)
> .build();
> ```
>
> `DashScopeChatModel` 同样支持此配置。对于 OpenAI 原生模型(GPT-4o 等)无需设置,默认行为即可正确处理。
#### 结构化输出路径选择

框架提供两条结构化输出路径:

| 路径 | 条件 | 机制 |
|------|------|------|
| **Native** | `supportsNativeStructuredOutput() = true` | 通过 `response_format` + `json_schema` 让模型直接输出合规 JSON |
| **Fallback**(默认) | `supportsNativeStructuredOutput() = false` | 注入 `generate_response` 合成工具,模型通过 tool call 返回结构化数据 |

当 native 路径失败(如模型返回 400),框架会**自动降级**到 fallback 路径,无需用户干预。

#### 各 Provider 默认行为

| Provider | `supportsNativeStructuredOutput` | 说明 |
|----------|----------------------------------|------|
| OpenAI (GPT-4o 等) | `true` | 原生支持 `json_schema` |
| OpenAI (DeepSeek/GLM formatter) | `false` | 不支持,自动走 fallback |
| DashScope | `false` | DashScope 原生端点仅支持 `json_object`,不支持 `json_schema`;框架默认走 fallback |
| Anthropic | `false`(默认) | — |

> **DashScope 用户注意**:DashScope 的思考模式(`enableThinking(true)`)不支持结构化输出,框架会强制走 fallback 路径。

#### 显式配置

如果确认你的模型/端点支持 `json_schema`,可以通过 builder 开启 native 路径:

```java
DashScopeChatModel model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.nativeStructuredOutput(true) // 显式开启 native json_schema 路径
.build();
```

#### 结构化输出与工具调用共存

当 Agent 同时注册了工具并请求结构化输出时,部分 OpenAI 兼容 API(如 Kimi、Deepseek 等)会优先遵循 `response_format` 约束而跳过工具调用。设置 `nativeStructuredOutputWithTools(false)` 可解决此问题:

```java
OpenAIChatModel model = OpenAIChatModel.builder()
.apiKey("...")
.baseUrl("https://api.moonshot.cn/v1")
.modelName("moonshot-v1-8k")
.nativeStructuredOutputWithTools(false)
.build();
```

`DashScopeChatModel` 同样支持此配置。对于 OpenAI 原生模型(GPT-4o 等)无需设置。

### Formatter

Expand Down
Loading