Skip to content

Commit a431f0d

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/agentscope-examples/boba-tea-shop/frontend/npm_and_yarn-b2936519f3
2 parents 8aa2e7c + d42b778 commit a431f0d

32 files changed

Lines changed: 2138 additions & 85 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,10 @@ protected Mono<Msg> summarizing() {
648648
postEvent -> {
649649
Msg finalMsg =
650650
postEvent
651-
.getSummaryMessage();
651+
.getSummaryMessage()
652+
.withGenerateReason(
653+
GenerateReason
654+
.MAX_ITERATIONS);
652655
memory.addMessage(finalMsg);
653656
return finalMsg;
654657
}));
@@ -747,12 +750,9 @@ private boolean isFinished(Msg msg) {
747750
List<ToolUseBlock> toolCalls = msg.getContentBlocks(ToolUseBlock.class);
748751

749752
// No tool calls - finished
750-
if (toolCalls.isEmpty()) {
751-
return true;
752-
}
753-
754-
// Has tool calls but none are in toolkit - finished
755-
return toolCalls.stream().noneMatch(tc -> toolkit.getTool(tc.getName()) != null);
753+
// If there are tool calls (even non-existent ones), continue to acting phase
754+
// where ToolExecutor will return "Tool not found" error for the model to see
755+
return toolCalls.isEmpty();
756756
}
757757

758758
/**

agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.agentscope.core.state.StateModule;
2727
import io.agentscope.core.tracing.TracerRegistry;
2828
import java.util.ArrayList;
29+
import java.util.Comparator;
2930
import java.util.List;
3031
import java.util.Map;
3132
import java.util.UUID;
@@ -98,6 +99,8 @@ public abstract class AgentBase implements StateModule, Agent {
9899
// Interrupt state management (available to all agents)
99100
private final AtomicBoolean interruptFlag = new AtomicBoolean(false);
100101
private final AtomicReference<Msg> userInterruptMessage = new AtomicReference<>(null);
102+
// Hook non-null
103+
private static final Comparator<Hook> HOOK_COMPARATOR = Comparator.comparingInt(Hook::priority);
101104

102105
/**
103106
* Constructor for AgentBase.
@@ -133,6 +136,7 @@ public AgentBase(String name, String description, boolean checkRunning, List<Hoo
133136
this.checkRunning = checkRunning;
134137
this.hooks = new CopyOnWriteArrayList<>(hooks != null ? hooks : List.of());
135138
this.hooks.addAll(systemHooks);
139+
sortHooks();
136140
}
137141

138142
@Override
@@ -474,9 +478,14 @@ public List<Hook> getHooks() {
474478
protected void addHook(Hook hook) {
475479
if (hook != null) {
476480
hooks.add(hook);
481+
sortHooks();
477482
}
478483
}
479484

485+
private void sortHooks() {
486+
this.hooks.sort(HOOK_COMPARATOR);
487+
}
488+
480489
/**
481490
* Remove a hook from this agent dynamically.
482491
*
@@ -498,7 +507,7 @@ protected void removeHook(Hook hook) {
498507
* @return Sorted list of hooks
499508
*/
500509
protected List<Hook> getSortedHooks() {
501-
return hooks.stream().sorted(java.util.Comparator.comparingInt(Hook::priority)).toList();
510+
return hooks;
502511
}
503512

504513
/**
@@ -692,7 +701,7 @@ private Flux<Event> createEventStream(StreamOptions options, Supplier<Mono<Msg>>
692701
new StreamingHook(sink, options);
693702

694703
// Add temporary hook
695-
hooks.add(streamingHook);
704+
addHook(streamingHook);
696705

697706
// Use Mono.defer to ensure trace context propagation
698707
// while maintaining streaming hook functionality

agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIConversationMerger.java

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -83,38 +83,16 @@ public OpenAIMessage mergeToUserMessage(
8383
StringBuilder textBuffer = new StringBuilder();
8484
textBuffer.append(conversationHistoryPrompt);
8585
textBuffer.append(HISTORY_START_TAG).append("\n");
86-
87-
// Process all messages EXCEPT the last one as history
88-
int lastIndex = msgs.size() - 1;
89-
90-
// Append history messages
91-
if (lastIndex > 0) {
92-
for (int i = 0; i < lastIndex; i++) {
93-
processMessage(
94-
msgs.get(i),
95-
roleFormatter,
96-
toolResultConverter,
97-
textBuffer,
98-
allParts,
99-
true);
100-
}
101-
}
102-
textBuffer.append(HISTORY_END_TAG).append("\n");
103-
104-
// Process the last message (current turn)
105-
if (lastIndex >= 0) {
106-
// Include prefix only if there is history (multi-turn context)
107-
// or if it's explicitly needed. For single-turn user queries,
108-
// omitting the prefix makes it look like a standard chat request.
109-
boolean includePrefix = lastIndex > 0;
86+
// Include prefix only if there is history (multi-turn context)
87+
// For single-turn user queries,
88+
// omitting the prefix makes it look like a standard chat request.
89+
boolean includePrefix = msgs.size() > 1;
90+
// Process all messages as history (similar to DashScopeConversationMerger)
91+
for (Msg msg : msgs) {
11092
processMessage(
111-
msgs.get(lastIndex),
112-
roleFormatter,
113-
toolResultConverter,
114-
textBuffer,
115-
allParts,
116-
includePrefix);
93+
msg, roleFormatter, toolResultConverter, textBuffer, allParts, includePrefix);
11794
}
95+
textBuffer.append(HISTORY_END_TAG).append("\n");
11896

11997
// Flush remaining text
12098
if (textBuffer.length() > 0) {
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.model.tts;
17+
18+
import java.util.Locale;
19+
import java.util.Random;
20+
import java.util.concurrent.ThreadLocalRandom;
21+
22+
/**
23+
* Predefined voices for Qwen3 TTS Flash / Realtime models.
24+
*
25+
* <p>The {@code voiceId} values correspond to the {@code voice} parameter
26+
* accepted by qwen3-tts-flash and qwen3-tts-flash-realtime.
27+
*/
28+
public enum Qwen3TTSFlashVoice {
29+
30+
/**
31+
* 芊悦 (Cherry) - A sunny, positive, friendly, and natural young woman.
32+
*/
33+
CHERRY("Cherry", "芊悦", Gender.FEMALE, "A sunny, positive, friendly, and natural young woman"),
34+
35+
/**
36+
* 晨煦 (Ethan) - A bright, warm, energetic, and vibrant male voice with a standard Mandarin pronunciation and a slight northern accent.
37+
*/
38+
ETHAN(
39+
"Ethan",
40+
"晨煦",
41+
Gender.MALE,
42+
"A bright, warm, energetic, and vibrant male voice with a standard Mandarin"
43+
+ " pronunciation and a slight northern accent"),
44+
45+
/**
46+
* 不吃鱼 (Nofish) - A male designer who cannot pronounce retroflex sounds.
47+
*/
48+
NOFISH("Nofish", "不吃鱼", Gender.MALE, "A male designer who cannot pronounce retroflex sounds"),
49+
50+
/**
51+
* 詹妮弗 (Jennifer) - A premium, cinematic American English female voice.
52+
*/
53+
JENNIFER(
54+
"Jennifer", "詹妮弗", Gender.FEMALE, "A premium, cinematic American English female voice"),
55+
56+
/**
57+
* 甜茶 (Ryan) - A rhythmic and dramatic voice with a sense of realism and tension.
58+
*/
59+
RYAN(
60+
"Ryan",
61+
"甜茶",
62+
Gender.MALE,
63+
"A rhythmic and dramatic voice with a sense of realism and tension"),
64+
65+
/**
66+
* 卡捷琳娜 (Katerina) - A mature female voice with a rich rhythm and lingering resonance.
67+
*/
68+
KATERINA(
69+
"Katerina",
70+
"卡捷琳娜",
71+
Gender.FEMALE,
72+
"A mature female voice with a rich rhythm and lingering resonance"),
73+
74+
/**
75+
* 墨讲师 (Elias) - A voice that maintains academic rigor while using storytelling techniques to transform complex knowledge into digestible cognitive modules.
76+
*/
77+
ELIAS(
78+
"Elias",
79+
"墨讲师",
80+
Gender.MALE,
81+
"A voice that maintains academic rigor while using storytelling techniques to transform"
82+
+ " complex knowledge into digestible cognitive modules"),
83+
84+
/**
85+
* 上海-阿珍 (Jada) - An energetic woman from Shanghai.
86+
*/
87+
JADA("Jada", "上海-阿珍", Gender.FEMALE, "An energetic woman from Shanghai"),
88+
89+
/**
90+
* 北京-晓东 (Dylan) - A teenage boy who grew up in the hutongs of Beijing.
91+
*/
92+
DYLAN("Dylan", "北京-晓东", Gender.MALE, "A teenage boy who grew up in the hutongs of Beijing"),
93+
94+
/**
95+
* 四川-晴儿 (Sunny) - The voice of a Sichuan girl whose sweetness melts your heart.
96+
*/
97+
SUNNY(
98+
"Sunny",
99+
"四川-晴儿",
100+
Gender.FEMALE,
101+
"The voice of a Sichuan girl whose sweetness melts your heart"),
102+
103+
/**
104+
* 南京-老李 (li) - Patient male yoga instructor.
105+
*/
106+
LI("li", "南京-老李", Gender.MALE, "Patient male yoga instructor"),
107+
108+
/**
109+
* 陕西-秦川 (Marcus) - A voice that is broad-faced and brief-spoken, sincere-hearted and deep-voiced—the authentic flavor of Shaanxi.
110+
*/
111+
MARCUS(
112+
"Marcus",
113+
"陕西-秦川",
114+
Gender.MALE,
115+
"A voice that is broad-faced and brief-spoken, sincere-hearted and deep-voiced—the"
116+
+ " authentic flavor of Shaanxi"),
117+
118+
/**
119+
* 闽南-阿杰 (Roy) - The voice of a humorous, straightforward, and lively young Taiwanese man.
120+
*/
121+
ROY(
122+
"Roy",
123+
"闽南-阿杰",
124+
Gender.MALE,
125+
"The voice of a humorous, straightforward, and lively young Taiwanese man"),
126+
127+
/**
128+
* 天津-李彼得 (Peter) - The voice of a professional straight man in Tianjin crosstalk.
129+
*/
130+
PETER(
131+
"Peter",
132+
"天津-李彼得",
133+
Gender.MALE,
134+
"The voice of a professional straight man in Tianjin crosstalk"),
135+
136+
/**
137+
* 粤语-阿强 (Rocky) - The voice of the humorous and witty Rocky, here for online chatting.
138+
*/
139+
ROCKY(
140+
"Rocky",
141+
"粤语-阿强",
142+
Gender.MALE,
143+
"The voice of the humorous and witty Rocky, here for online chatting"),
144+
145+
/**
146+
* 粤语-阿清 (Kiki) - A sweet female companion from Hong Kong.
147+
*/
148+
KIKI("Kiki", "粤语-阿清", Gender.FEMALE, "A sweet female companion from Hong Kong"),
149+
150+
/**
151+
* 四川-程川 (Eric) - An unconventional man from Chengdu, Sichuan.
152+
*/
153+
ERIC("Eric", "四川-程川", Gender.MALE, "An unconventional man from Chengdu, Sichuan");
154+
155+
private final String voiceId;
156+
private final String displayName;
157+
private final Gender gender;
158+
private final String description;
159+
160+
Qwen3TTSFlashVoice(String voiceId, String displayName, Gender gender, String description) {
161+
this.voiceId = voiceId;
162+
this.displayName = displayName;
163+
this.gender = gender;
164+
this.description = description;
165+
}
166+
167+
/**
168+
* Voice id to use as the {@code voice} parameter in DashScope TTS requests.
169+
*/
170+
public String getVoiceId() {
171+
return voiceId;
172+
}
173+
174+
/**
175+
* Human friendly display name (typically Chinese).
176+
*/
177+
public String getDisplayName() {
178+
return displayName;
179+
}
180+
181+
/**
182+
* Gender of this voice (for informational / filtering purposes).
183+
*/
184+
public Gender getGender() {
185+
return gender;
186+
}
187+
188+
/**
189+
* Short description of the voice characteristics.
190+
*/
191+
public String getDescription() {
192+
return description;
193+
}
194+
195+
/**
196+
* Find a voice enum by its voiceId (case-insensitive).
197+
*
198+
* @param voiceId the voice id string, e.g. "Cherry"
199+
* @return matching enum value, or {@code null} if not found
200+
*/
201+
public static Qwen3TTSFlashVoice fromVoiceId(String voiceId) {
202+
if (voiceId == null || voiceId.isEmpty()) {
203+
return null;
204+
}
205+
String normalized = voiceId.toLowerCase(Locale.ROOT);
206+
for (Qwen3TTSFlashVoice v : values()) {
207+
if (v.voiceId.toLowerCase(Locale.ROOT).equals(normalized)) {
208+
return v;
209+
}
210+
}
211+
return null;
212+
}
213+
214+
/**
215+
* Pick a random voice using {@link ThreadLocalRandom}.
216+
*/
217+
public static Qwen3TTSFlashVoice random() {
218+
return random(ThreadLocalRandom.current());
219+
}
220+
221+
/**
222+
* Pick a random voice using the provided {@link Random} instance.
223+
*/
224+
public static Qwen3TTSFlashVoice random(Random random) {
225+
Qwen3TTSFlashVoice[] all = values();
226+
if (all.length == 0) {
227+
throw new IllegalStateException("No Qwen3TTSFlashVoice defined");
228+
}
229+
int idx = random.nextInt(all.length);
230+
return all[idx];
231+
}
232+
233+
/** Simple gender enum for voices. */
234+
public enum Gender {
235+
MALE,
236+
FEMALE
237+
}
238+
}

agentscope-core/src/main/java/io/agentscope/core/skill/repository/ClasspathSkillRepository.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ protected ClasspathSkillRepository(String resourcePath, String source, ClassLoad
139139
if ("jar".equals(uri.getScheme())) {
140140
this.isJar = true;
141141
this.fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
142-
String actualResourcePath = uri.getSchemeSpecificPart().split("!")[1];
142+
String schemeSpecificUriPath = uri.getSchemeSpecificPart();
143+
String actualResourcePath =
144+
schemeSpecificUriPath.substring(schemeSpecificUriPath.lastIndexOf("!") + 1);
143145
logger.info("Actual resource path: {}", actualResourcePath);
144146
this.skillBasePath = fileSystem.getPath(actualResourcePath);
145147
} else {

0 commit comments

Comments
 (0)