+ * 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
+ *
+ * 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 com.easyagents.llm.deepseek;
+
+import com.easyagents.core.model.chat.OpenAICompatibleChatModel;
+import com.easyagents.core.model.chat.ChatInterceptor;
+import com.easyagents.core.model.chat.GlobalChatInterceptors;
+
+import java.util.List;
+
+/**
+ * @author huangjf
+ * @version : v1.0
+ */
+public class DeepseekChatModel extends OpenAICompatibleChatModel {
+
+
+ /**
+ * 构造一个聊天模型实例,不使用实例级拦截器。
+ *
+ * @param config 聊天模型配置
+ */
+ public DeepseekChatModel(DeepseekConfig config) {
+ super(config);
+ }
+
+ /**
+ * 构造一个聊天模型实例,并指定实例级拦截器。
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.deepseek;
+
+import com.easyagents.core.model.chat.ChatConfig;
+
+/**
+ * @author huangjf
+ * @version : v1.0
+ */
+public class DeepseekConfig extends ChatConfig {
+
+ private static final String DEFAULT_MODEL = "deepseek-chat";
+ private static final String DEFAULT_ENDPOINT = "https://api.deepseek.com";
+ private static final String DEFAULT_REQUEST_PATH = "/chat/completions";
+
+ public DeepseekConfig() {
+ setEndpoint(DEFAULT_ENDPOINT);
+ setRequestPath(DEFAULT_REQUEST_PATH);
+ setModel(DEFAULT_MODEL);
+ }
+
+}
diff --git a/easy-agents-chat/easy-agents-chat-deepseek/src/test/java/com/easyagents/llm/deepseek/DeepseekTest.java b/easy-agents-chat/easy-agents-chat-deepseek/src/test/java/com/easyagents/llm/deepseek/DeepseekTest.java
new file mode 100644
index 0000000..67cfe6f
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-deepseek/src/test/java/com/easyagents/llm/deepseek/DeepseekTest.java
@@ -0,0 +1,92 @@
+package com.easyagents.llm.deepseek;
+
+import com.easyagents.core.message.UserMessage;
+import com.easyagents.core.model.client.StreamContext;
+import com.easyagents.core.model.chat.ChatModel;
+import com.easyagents.core.model.chat.StreamResponseListener;
+import com.easyagents.core.model.chat.tool.annotation.ToolDef;
+import com.easyagents.core.model.chat.tool.annotation.ToolParam;
+import com.easyagents.core.model.chat.response.AiMessageResponse;
+import com.easyagents.core.message.SystemMessage;
+import com.easyagents.core.prompt.MemoryPrompt;
+import com.easyagents.core.prompt.SimplePrompt;
+import com.easyagents.core.util.StringUtil;
+
+import java.util.Scanner;
+
+public class DeepseekTest {
+
+
+ @ToolDef(name = "get_the_weather_info", description = "get the weather info")
+ public static String getWeatherInfo(@ToolParam(name = "city", description = "城市名称") String name) {
+ //在这里,我们应该通过第三方接口调用 api 信息
+ return name + "的天气是阴转多云。 ";
+ }
+
+ @ToolDef(name = "get_holiday_balance", description = "获取假期余额")
+ public static String getHolidayBalance() {
+ //在这里,我们应该通过第三方接口调用 api 信息
+ String username = "michael";
+ return username + "你的年假还剩余3天,有效期至26年1月。调休假剩余1天,长期有效。 ";
+ }
+
+ public static ChatModel getLLM() {
+ DeepseekConfig deepseekConfig = new DeepseekConfig();
+ deepseekConfig.setEndpoint("https://api.siliconflow.cn/v1");
+ deepseekConfig.setApiKey("*********************");
+ deepseekConfig.setModel("Pro/deepseek-ai/DeepSeek-V3");
+ deepseekConfig.setLogEnabled(true);
+ return new DeepseekChatModel(deepseekConfig);
+ }
+
+ public static void chatHr() {
+ ChatModel chatModel = getLLM();
+ MemoryPrompt prompt = new MemoryPrompt();
+ // 加入system
+ prompt.addMessage(new SystemMessage("你是一个人事助手小智,专注于为用户提供高效、精准的信息查询和问题解答服务。"));
+ System.out.println("我是小智,你的人事小助手!请尽情吩咐小智!");
+ Scanner scanner = new Scanner(System.in);
+ String userInput = scanner.nextLine();
+ while (userInput != null) {
+ // 第二步:创建 HumanMessage,并添加方法调用
+ UserMessage userMessage = new UserMessage(userInput);
+ userMessage.addToolsFromClass(DeepseekTest.class);
+ // 第三步:将 HumanMessage 添加到 HistoriesPrompt 中
+ prompt.addMessage(userMessage);
+ // 第四步:调用 chatStream 方法,进行对话
+ chatModel.chatStream(prompt, new StreamResponseListener() {
+ @Override
+ public void onMessage(StreamContext context, AiMessageResponse response) {
+ if (StringUtil.hasText(response.getMessage().getContent())) {
+ System.out.print(response.getMessage().getContent());
+ }
+ if (response.getMessage().isFinalDelta()) {
+ System.out.println(response);
+ System.out.println("------");
+ }
+ }
+
+ @Override
+ public void onStop(StreamContext context) {
+ System.out.println("stop!!!------");
+ }
+ });
+ userInput = scanner.nextLine();
+ }
+
+ }
+
+
+ public static void functionCall() {
+ ChatModel chatModel = getLLM();
+ SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
+ prompt.addToolsFromClass(DeepseekTest.class);
+ AiMessageResponse response = chatModel.chat(prompt);
+ System.out.println(response.executeToolCallsAndGetResults());
+ }
+
+ public static void main(String[] args) {
+// functionCall();
+ chatHr();
+ }
+}
diff --git a/easy-agents-chat/easy-agents-chat-ollama/pom.xml b/easy-agents-chat/easy-agents-chat-ollama/pom.xml
new file mode 100644
index 0000000..88b9d7f
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-ollama/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+ com.easyagents
+ easy-agents-chat
+ ${revision}
+
+
+ easy-agents-chat-ollama
+ easy-agents-chat-ollama
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ com.easyagents
+ easy-agents-core
+ compile
+
+
+ junit
+ junit
+ test
+
+
+
+
diff --git a/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaChatConfig.java b/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaChatConfig.java
new file mode 100644
index 0000000..5416f25
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaChatConfig.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.ollama;
+
+import com.easyagents.core.model.chat.ChatConfig;
+
+public class OllamaChatConfig extends ChatConfig {
+
+ private static final String DEFAULT_PROVIDER = "ollama";
+ private static final String DEFAULT_ENDPOINT = "https://localhost:11434";
+ private static final String DEFAULT_REQUEST_PATH = "/v1/chat/completions";
+
+ public OllamaChatConfig() {
+ setProvider(DEFAULT_PROVIDER);
+ setEndpoint(DEFAULT_ENDPOINT);
+ setRequestPath(DEFAULT_REQUEST_PATH);
+ }
+
+}
diff --git a/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaChatModel.java b/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaChatModel.java
new file mode 100644
index 0000000..c2fc7ac
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaChatModel.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.ollama;
+
+import com.easyagents.core.model.chat.OpenAICompatibleChatModel;
+import com.easyagents.core.model.chat.ChatInterceptor;
+import com.easyagents.core.model.chat.GlobalChatInterceptors;
+import com.easyagents.core.model.client.ChatRequestSpecBuilder;
+
+
+import java.util.List;
+
+public class OllamaChatModel extends OpenAICompatibleChatModel {
+
+ /**
+ * 构造一个聊天模型实例,不使用实例级拦截器。
+ *
+ * @param config 聊天模型配置
+ */
+ public OllamaChatModel(OllamaChatConfig config) {
+ super(config);
+ }
+
+ /**
+ * 构造一个聊天模型实例,并指定实例级拦截器。
+ *
+ * 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
+ * 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
+ *
+ * @param config 聊天模型配置
+ * @param userInterceptors 实例级拦截器列表
+ */
+ public OllamaChatModel(OllamaChatConfig config, List userInterceptors) {
+ super(config, userInterceptors);
+ }
+
+
+ @Override
+ public ChatRequestSpecBuilder getChatRequestSpecBuilder() {
+ return new OllamaRequestSpecBuilder();
+ }
+
+}
diff --git a/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaRequestSpecBuilder.java b/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaRequestSpecBuilder.java
new file mode 100644
index 0000000..8ce0f50
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-ollama/src/main/java/com/easyagents/llm/ollama/OllamaRequestSpecBuilder.java
@@ -0,0 +1,21 @@
+package com.easyagents.llm.ollama;
+
+import com.easyagents.core.model.chat.ChatConfig;
+import com.easyagents.core.model.chat.ChatOptions;
+import com.easyagents.core.model.client.OpenAIChatRequestSpecBuilder;
+import com.easyagents.core.prompt.Prompt;
+import com.easyagents.core.util.Maps;
+
+public class OllamaRequestSpecBuilder extends OpenAIChatRequestSpecBuilder {
+ protected Maps buildBaseParamsOfRequestBody(Prompt prompt, ChatOptions options, ChatConfig config) {
+ Maps params = super.buildBaseParamsOfRequestBody(prompt, options, config);
+ params.setIf(!options.isStreaming(), "stream", false);
+
+ // 支持思考
+ if (config.isSupportThinking()) {
+ params.setIf(options.getThinkingEnabled() != null, "thinking", options.getThinkingEnabled());
+ }
+
+ return params;
+ }
+}
diff --git a/easy-agents-chat/easy-agents-chat-ollama/src/test/java/com/easyagents/llm/ollama/OllamaChatModelTest.java b/easy-agents-chat/easy-agents-chat-ollama/src/test/java/com/easyagents/llm/ollama/OllamaChatModelTest.java
new file mode 100644
index 0000000..b655e1f
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-ollama/src/test/java/com/easyagents/llm/ollama/OllamaChatModelTest.java
@@ -0,0 +1,94 @@
+package com.easyagents.llm.ollama;
+
+import com.easyagents.core.message.AiMessage;
+import com.easyagents.core.model.chat.ChatModel;
+import com.easyagents.core.model.chat.response.AiMessageResponse;
+import com.easyagents.core.model.exception.ModelException;
+import com.easyagents.core.prompt.SimplePrompt;
+import org.junit.Test;
+
+public class OllamaChatModelTest {
+
+ @Test(expected = ModelException.class)
+ public void testChat() {
+ OllamaChatConfig config = new OllamaChatConfig();
+ config.setEndpoint("http://localhost:11434");
+ config.setModel("llama3");
+ config.setLogEnabled(true);
+
+ ChatModel chatModel = new OllamaChatModel(config);
+ String chat = chatModel.chat("Why is the sky blue?");
+ System.out.println(">>>" + chat);
+ }
+
+
+ @Test
+ public void testChatStream() throws InterruptedException {
+ OllamaChatConfig config = new OllamaChatConfig();
+ config.setEndpoint("http://localhost:11434");
+ config.setModel("llama3");
+ config.setLogEnabled(true);
+
+ ChatModel chatModel = new OllamaChatModel(config);
+ chatModel.chatStream("Why is the sky blue?", (context, response) -> System.out.println(response.getMessage().getContent()));
+
+ Thread.sleep(2000);
+ }
+
+
+ @Test
+ public void testFunctionCall1() throws InterruptedException {
+ OllamaChatConfig config = new OllamaChatConfig();
+ config.setEndpoint("http://localhost:11434");
+ config.setModel("llama3.1");
+ config.setLogEnabled(true);
+
+ ChatModel chatModel = new OllamaChatModel(config);
+
+ SimplePrompt prompt = new SimplePrompt("What's the weather like in Beijing?");
+ prompt.addToolsFromClass(WeatherFunctions.class);
+ AiMessageResponse response = chatModel.chat(prompt);
+
+ System.out.println(response.executeToolCallsAndGetResults());
+ }
+
+
+ @Test
+ public void testFunctionCall2() throws InterruptedException {
+ OllamaChatConfig config = new OllamaChatConfig();
+ config.setEndpoint("http://localhost:11434");
+ config.setModel("llama3.1");
+ config.setLogEnabled(true);
+
+ ChatModel chatModel = new OllamaChatModel(config);
+
+ SimplePrompt prompt = new SimplePrompt("What's the weather like in Beijing?");
+ prompt.addToolsFromClass(WeatherFunctions.class);
+ AiMessageResponse response = chatModel.chat(prompt);
+
+ if (response.hasToolCalls()) {
+ prompt.setToolMessages(response.executeToolCallsAndGetToolMessages());
+ AiMessageResponse response1 = chatModel.chat(prompt);
+ System.out.println(response1.getMessage().getContent());
+ }
+ }
+
+
+ @Test
+ public void testVisionModel() {
+ OllamaChatConfig config = new OllamaChatConfig();
+ config.setEndpoint("http://localhost:11434");
+ config.setModel("llava");
+ config.setLogEnabled(true);
+
+ ChatModel chatModel = new OllamaChatModel(config);
+
+ SimplePrompt imagePrompt = new SimplePrompt("What's in the picture?");
+ imagePrompt.addImageUrl("https://agentsflex.com/assets/images/logo.png");
+
+ AiMessageResponse response = chatModel.chat(imagePrompt);
+ AiMessage message = response == null ? null : response.getMessage();
+ System.out.println(message);
+ }
+
+}
diff --git a/easy-agents-chat/easy-agents-chat-ollama/src/test/java/com/easyagents/llm/ollama/WeatherFunctions.java b/easy-agents-chat/easy-agents-chat-ollama/src/test/java/com/easyagents/llm/ollama/WeatherFunctions.java
new file mode 100644
index 0000000..2dbb2f2
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-ollama/src/test/java/com/easyagents/llm/ollama/WeatherFunctions.java
@@ -0,0 +1,22 @@
+package com.easyagents.llm.ollama;
+
+import com.easyagents.core.model.chat.tool.annotation.ToolDef;
+import com.easyagents.core.model.chat.tool.annotation.ToolParam;
+
+public class WeatherFunctions {
+
+ @ToolDef(name = "get_the_weather_info", description = "get the weather info")
+ public static String getWeatherInfo(
+ @ToolParam(name = "city", description = "the city name") String name
+ ) {
+ return "Snowy days";
+ }
+
+
+ @ToolDef(name = "get_the_temperature", description = "get the temperature")
+ public static String getTemperature(
+ @ToolParam(name = "city", description = "the city name") String name
+ ) {
+ return "The temperature in " + name + " is 15°C";
+ }
+}
diff --git a/easy-agents-chat/easy-agents-chat-openai/pom.xml b/easy-agents-chat/easy-agents-chat-openai/pom.xml
new file mode 100644
index 0000000..cc198fd
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-openai/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ com.easyagents
+ easy-agents-chat
+ ${revision}
+
+
+ easy-agents-chat-openai
+ easy-agents-chat-openai
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+ com.easyagents
+ easy-agents-core
+
+
+ junit
+ junit
+ test
+
+
+
+
diff --git a/easy-agents-chat/easy-agents-chat-openai/src/main/java/com/easyagents/llm/openai/OpenAIChatConfig.java b/easy-agents-chat/easy-agents-chat-openai/src/main/java/com/easyagents/llm/openai/OpenAIChatConfig.java
new file mode 100644
index 0000000..ab30275
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-openai/src/main/java/com/easyagents/llm/openai/OpenAIChatConfig.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.openai;
+
+import com.easyagents.core.model.chat.ChatConfig;
+import com.easyagents.core.model.chat.ChatInterceptor;
+import com.easyagents.core.util.StringUtil;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * OpenAI 聊天模型的配置类,支持通过 Builder 模式创建配置或直接构建 {@link OpenAIChatModel}。
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.openai;
+
+import com.easyagents.core.model.chat.BaseChatModel;
+import com.easyagents.core.model.chat.OpenAICompatibleChatModel;
+import com.easyagents.core.model.chat.ChatInterceptor;
+import com.easyagents.core.model.chat.GlobalChatInterceptors;
+
+import java.util.List;
+
+/**
+ * OpenAI 聊天模型实现。
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.qwen;
+
+import com.easyagents.core.model.chat.ChatConfig;
+
+public class QwenChatConfig extends ChatConfig {
+
+ private static final String DEFAULT_MODEL = "qwen-turbo";
+ private static final String DEFAULT_ENDPOINT = "https://dashscope.aliyuncs.com";
+ private static final String DEFAULT_REQUEST_PATH = "/compatible-mode/v1/chat/completions";
+
+ public QwenChatConfig() {
+ setEndpoint(DEFAULT_ENDPOINT);
+ setRequestPath(DEFAULT_REQUEST_PATH);
+ setModel(DEFAULT_MODEL);
+ }
+
+}
diff --git a/easy-agents-chat/easy-agents-chat-qwen/src/main/java/com/easyagents/llm/qwen/QwenChatModel.java b/easy-agents-chat/easy-agents-chat-qwen/src/main/java/com/easyagents/llm/qwen/QwenChatModel.java
new file mode 100644
index 0000000..e903678
--- /dev/null
+++ b/easy-agents-chat/easy-agents-chat-qwen/src/main/java/com/easyagents/llm/qwen/QwenChatModel.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.llm.qwen;
+
+import com.easyagents.core.model.chat.OpenAICompatibleChatModel;
+import com.easyagents.core.model.chat.ChatInterceptor;
+import com.easyagents.core.model.chat.GlobalChatInterceptors;
+import com.easyagents.core.model.client.ChatRequestSpecBuilder;
+
+import java.util.List;
+
+public class QwenChatModel extends OpenAICompatibleChatModel {
+
+
+ /**
+ * 构造一个聊天模型实例,不使用实例级拦截器。
+ *
+ * @param config 聊天模型配置
+ */
+ public QwenChatModel(QwenChatConfig config) {
+ super(config);
+ }
+
+ /**
+ * 构造一个聊天模型实例,并指定实例级拦截器。
+ *
+ * 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
+ *
+ * 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 com.easyagents.core;
+
+public class Consts {
+
+ public static final String VERSION = "2.0.0-rc.6";
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/IAgent.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/IAgent.java
new file mode 100644
index 0000000..0f08c74
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/IAgent.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent;
+
+public interface IAgent {
+
+ void execute();
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActAgent.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActAgent.java
new file mode 100644
index 0000000..2e89959
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActAgent.java
@@ -0,0 +1,564 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent.react;
+
+import com.easyagents.core.message.Message;
+import com.easyagents.core.message.UserMessage;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReActAgentState implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ String userQuery;
+ List messageHistory;
+ int iterationCount = 0;
+ int maxIterations;
+ boolean streamable;
+ String promptTemplate;
+ boolean continueOnActionInvokeError;
+
+ public String getUserQuery() {
+ return userQuery;
+ }
+
+ public void setUserQuery(String userQuery) {
+ this.userQuery = userQuery;
+ }
+
+ public List getMessageHistory() {
+ return messageHistory;
+ }
+
+ public void setMessageHistory(List messageHistory) {
+ this.messageHistory = messageHistory;
+ }
+
+ public void addMessage(UserMessage message) {
+ if (messageHistory == null) {
+ messageHistory = new ArrayList<>();
+ }
+ messageHistory.add(message);
+ }
+
+ public int getIterationCount() {
+ return iterationCount;
+ }
+
+ public void setIterationCount(int iterationCount) {
+ this.iterationCount = iterationCount;
+ }
+
+ public int getMaxIterations() {
+ return maxIterations;
+ }
+
+ public void setMaxIterations(int maxIterations) {
+ this.maxIterations = maxIterations;
+ }
+
+ public boolean isStreamable() {
+ return streamable;
+ }
+
+ public void setStreamable(boolean streamable) {
+ this.streamable = streamable;
+ }
+
+ public String getPromptTemplate() {
+ return promptTemplate;
+ }
+
+ public void setPromptTemplate(String promptTemplate) {
+ this.promptTemplate = promptTemplate;
+ }
+
+ public boolean isContinueOnActionInvokeError() {
+ return continueOnActionInvokeError;
+ }
+
+ public void setContinueOnActionInvokeError(boolean continueOnActionInvokeError) {
+ this.continueOnActionInvokeError = continueOnActionInvokeError;
+ }
+
+ public String toJSON() {
+ return JSON.toJSONString(this, JSONWriter.Feature.WriteClassName);
+ }
+
+ public static ReActAgentState fromJSON(String json) {
+ return JSON.parseObject(json, ReActAgentState.class, JSONReader.Feature.SupportClassForName);
+ }
+
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActAgentTool.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActAgentTool.java
new file mode 100644
index 0000000..f5ed1c0
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActAgentTool.java
@@ -0,0 +1,123 @@
+///*
+// * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+// *
+// * 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
+// *
+// * 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 com.easyagents.core.agent.react;
+//
+//import com.easyagents.core.model.chat.tool.Parameter;
+//import com.easyagents.core.model.chat.tool.Tool;
+//import com.easyagents.core.model.chat.tool.ToolContextHolder;
+//
+//import java.util.Map;
+//import java.util.concurrent.CountDownLatch;
+//import java.util.concurrent.TimeUnit;
+//
+//public class ReActAgentTool implements Tool {
+//
+// public static final String PARENT_AGENT_KEY = "__parent_react_agent";
+//
+// private final ReActAgent subAgent;
+// private String name;
+// private String description;
+//
+// public ReActAgentTool(ReActAgent subAgent) {
+// this.subAgent = subAgent;
+// }
+//
+//
+// @Override
+// public String getName() {
+// return name;
+// }
+//
+// @Override
+// public String getDescription() {
+// return description;
+// }
+//
+// @Override
+// public Parameter[] getParameters() {
+// return new Parameter[0];
+// }
+//
+// @Override
+// public Object invoke(Map argsMap) {
+// ReActAgent parentAgent = ToolContextHolder.currentContext().getAttribute(PARENT_AGENT_KEY);
+//
+//
+// if (parentAgent != null) {
+// // @todo 获取父 agent 的监听器 和 历史消息,传入给 sub Agent
+// }
+//
+// SyncReActListener listener = new SyncReActListener();
+// subAgent.addListener(listener);
+//
+// subAgent.execute();
+//
+// try {
+// return listener.getFinalAnswer(1000, TimeUnit.MILLISECONDS);
+// } catch (InterruptedException e) {
+// Thread.currentThread().interrupt();
+// throw new RuntimeException("ReActAgent execution was interrupted", e);
+// }
+// }
+//
+//
+// // 同步监听器(内部类或独立类)
+// public static class SyncReActListener implements ReActAgentListener {
+// private final CountDownLatch latch = new CountDownLatch(1);
+// private String finalAnswer;
+// private Exception error;
+//
+// @Override
+// public void onFinalAnswer(String answer) {
+// this.finalAnswer = answer;
+// latch.countDown();
+// }
+//
+// @Override
+// public void onError(Exception e) {
+// this.error = e;
+// latch.countDown();
+// }
+//
+// @Override
+// public void onMaxIterationsReached() {
+// this.error = new RuntimeException("ReActAgent reached max iterations without final answer");
+// latch.countDown();
+// }
+//
+// // 其他回调可留空
+// @Override
+// public void onActionStart(ReActStep step) {
+// }
+//
+// @Override
+// public void onActionEnd(ReActStep step, Object result) {
+// }
+//
+// public String getFinalAnswer(long timeout, TimeUnit unit) throws InterruptedException {
+// if (!latch.await(timeout, unit)) {
+// throw new RuntimeException("ReActAgent execution timed out");
+// }
+// if (error != null) {
+// throw new RuntimeException("ReActAgent execution failed", error);
+// }
+// if (finalAnswer == null) {
+// throw new RuntimeException("ReActAgent did not produce a final answer");
+// }
+// return finalAnswer;
+// }
+// }
+//}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActMessageBuilder.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActMessageBuilder.java
new file mode 100644
index 0000000..1f0fa10
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActMessageBuilder.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent.react;
+
+import com.easyagents.core.message.UserMessage;
+import com.easyagents.core.model.chat.tool.Tool;
+import com.easyagents.core.message.Message;
+
+import java.util.List;
+
+public class ReActMessageBuilder {
+
+
+ /**
+ * 构建 ReAct 开始消息
+ *
+ * @param prompt 提示词
+ * @param tools 函数列表
+ * @param userQuery 用户问题
+ * @return 返回 HumanMessage
+ */
+ public Message buildStartMessage(String prompt, List tools, String userQuery) {
+ UserMessage message = new UserMessage(prompt);
+ message.addMetadata("tools", tools);
+ message.addMetadata("user_input", userQuery);
+ message.addMetadata("type", "reActWrapper");
+ return message;
+ }
+
+
+ /**
+ * 构建 JSON 解析错误消息,用于 json 发送错误时,让 AI 自动修正
+ *
+ * @param e 错误信息
+ * @param step 发送错误的步骤
+ * @return 返回 HumanMessage
+ */
+ public Message buildJsonParserErrorMessage(Exception e, ReActStep step) {
+ String errorMsg = "JSON 解析失败: " + e.getMessage() + ", 原始内容: " + step.getActionInput();
+ String observation = "Action:" + step.getAction() + "\n"
+ + "Action Input:" + step.getActionInput() + "\n"
+ + "Error:" + errorMsg + "\n"
+ + "请检查你的 Action Input 格式是否正确,并纠正 JSON 内容重新生成响应。\n";
+ UserMessage userMessage = new UserMessage(observation + "请继续推理下一步。");
+ userMessage.addMetadata("type", "reActObservation");
+ return userMessage;
+ }
+
+ /**
+ * 构建 Observation 消息,让 AI 自动思考
+ *
+ * @param step 步骤
+ * @param result 步骤结果
+ * @return 步骤结果消息
+ */
+ public Message buildObservationMessage(ReActStep step, Object result) {
+ String observation = buildObservationString(step, result);
+ UserMessage userMessage = new UserMessage(observation + "\n请继续推理下一步。");
+ userMessage.addMetadata("type", "reActObservation");
+ return userMessage;
+ }
+
+
+ /**
+ * 构建 Observation 字符串
+ *
+ * @param step 步骤
+ * @param result 步骤结果
+ * @return 步骤结果字符串
+ */
+ public static String buildObservationString(ReActStep step, Object result) {
+ return "Action:" + step.getAction() + "\n" +
+ "Action Input:" + step.getActionInput() + "\n" +
+ "Action Result:" + result + "\n";
+ }
+
+ /**
+ * 构建 Action 错误消息,用于 Action 错误时,让 AI 自动修正
+ *
+ * @param step 步骤
+ * @param e 错误信息
+ * @return 错误消息
+ */
+ public Message buildActionErrorMessage(ReActStep step, Exception e) {
+ // 将错误信息反馈给 AI,让其修正
+ String observation = buildObservationString(step, "Error: " + e.getMessage()) + "\n"
+ + "请根据错误信息调整参数并重新尝试。\n";
+ UserMessage userMessage = new UserMessage(observation + "请继续推理下一步。");
+ userMessage.addMetadata("type", "reActObservation");
+ return userMessage;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActStep.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActStep.java
new file mode 100644
index 0000000..6dc5542
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActStep.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent.react;
+
+public class ReActStep {
+
+ private String thought;
+ private String action;
+ private String actionInput;
+
+ public ReActStep() {
+ }
+
+ public ReActStep(String thought, String action, String actionInput) {
+ this.thought = thought;
+ this.action = action;
+ this.actionInput = actionInput;
+ }
+
+ public String getThought() {
+ return thought;
+ }
+
+ public void setThought(String thought) {
+ this.thought = thought;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getActionInput() {
+ return actionInput;
+ }
+
+ public void setActionInput(String actionInput) {
+ this.actionInput = actionInput;
+ }
+
+
+ @Override
+ public String toString() {
+ return "ReActStep{" +
+ "thought='" + thought + '\'' +
+ ", action='" + action + '\'' +
+ ", actionInput='" + actionInput + '\'' +
+ '}';
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActStepParser.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActStepParser.java
new file mode 100644
index 0000000..9976526
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/react/ReActStepParser.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent.react;
+
+import com.easyagents.core.model.chat.tool.Parameter;
+import com.easyagents.core.model.chat.tool.Tool;
+
+import java.util.List;
+
+/**
+ * ReAct Agent 工具函数辅助类
+ */
+public class Util {
+
+ /**
+ * 生成带缩进的空格字符串
+ */
+ public static String indent(int depth) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ sb.append(" "); // 2 spaces per level
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 基于工具列表生成结构化、LLM 友好的工具描述文本
+ */
+ public static String buildToolsDescription(List tools) {
+ StringBuilder sb = new StringBuilder();
+ for (Tool tool : tools) {
+ sb.append("### 工具名称: ").append(tool.getName()).append("\n");
+ sb.append("**描述**: ").append(tool.getDescription()).append("\n");
+ sb.append("**调用参数格式 (JSON 对象)**:\n");
+ sb.append("```json\n");
+
+ sb.append("{\n");
+ Parameter[] rootParams = tool.getParameters();
+ for (int i = 0; i < rootParams.length; i++) {
+ appendParameter(sb, rootParams[i], 1);
+ if (i < rootParams.length - 1) {
+ sb.append(",");
+ }
+ sb.append("\n");
+ }
+ sb.append("}\n");
+ sb.append("```\n\n");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 递归追加参数描述(支持 object 和 array)
+ */
+ private static void appendParameter(StringBuilder sb, Parameter param, int depth) {
+ String currentIndent = indent(depth);
+ String typeLabel = getTypeLabel(param);
+
+ // 构建注释信息
+ StringBuilder comment = new StringBuilder();
+ if (param.isRequired()) {
+ comment.append(" (必填)");
+ } else {
+ comment.append(" (可选)");
+ }
+
+ if (param.getDescription() != null && !param.getDescription().trim().isEmpty()) {
+ comment.append(" - ").append(param.getDescription().trim());
+ }
+
+ if (param.getEnums() != null && param.getEnums().length > 0) {
+ comment.append(" [可选值: ").append(String.join(", ", param.getEnums())).append("]");
+ }
+
+ String paramName = param.getName() != null ? param.getName() : "item";
+
+ // 判断是否为数组类型
+ boolean isArray = isArrayType(param.getType());
+
+ if (isArray && param.getChildren() != null && !param.getChildren().isEmpty()) {
+ // 数组元素为对象:描述其结构
+ sb.append(currentIndent).append("\"").append(paramName).append("\": [");
+ sb.append(comment).append("\n");
+
+ String innerIndent = indent(depth + 1);
+ sb.append(innerIndent).append("{\n");
+
+ List elementFields = param.getChildren();
+ for (int i = 0; i < elementFields.size(); i++) {
+ appendParameter(sb, elementFields.get(i), depth + 2);
+ if (i < elementFields.size() - 1) {
+ sb.append(",");
+ }
+ sb.append("\n");
+ }
+ sb.append(innerIndent).append("}\n");
+ sb.append(currentIndent).append("]");
+
+ } else if (isArray) {
+ // 简单类型数组
+ sb.append(currentIndent).append("\"").append(paramName).append("\": [ \"<")
+ .append(typeLabel).append(">")
+ .append(comment)
+ .append("\" ]");
+
+ } else if (param.getChildren() != null && !param.getChildren().isEmpty()) {
+ // 嵌套对象
+ sb.append(currentIndent).append("\"").append(paramName).append("\": {");
+ sb.append(comment).append("\n");
+
+ List children = param.getChildren();
+ for (int i = 0; i < children.size(); i++) {
+ appendParameter(sb, children.get(i), depth + 1);
+ if (i < children.size() - 1) {
+ sb.append(",");
+ }
+ sb.append("\n");
+ }
+ sb.append(currentIndent).append("}");
+
+ } else {
+ // 叶子字段(简单类型)
+ sb.append(currentIndent).append("\"").append(paramName).append("\": \"<")
+ .append(typeLabel).append(">")
+ .append(comment)
+ .append("\"");
+ }
+ }
+
+ /**
+ * 判断类型是否为数组
+ */
+ private static boolean isArrayType(String type) {
+ if (type == null) return false;
+ String lower = type.toLowerCase();
+ return "array".equals(lower) || "list".equals(lower);
+ }
+
+ /**
+ * 获取标准化的类型标签
+ */
+ private static String getTypeLabel(Parameter param) {
+ String type = param.getType();
+ if (type == null) return "string";
+
+ // 若有子字段,则视为 object
+ if (param.getChildren() != null && !param.getChildren().isEmpty()) {
+ return "object";
+ }
+
+ String lower = type.toLowerCase();
+ if ("string".equals(lower) || "str".equals(lower)) {
+ return "string";
+ } else if ("integer".equals(lower) || "int".equals(lower)) {
+ return "integer";
+ } else if ("number".equals(lower) || "float".equals(lower) || "double".equals(lower)) {
+ return "number";
+ } else if ("boolean".equals(lower) || "bool".equals(lower)) {
+ return "boolean";
+ } else if ("array".equals(lower) || "list".equals(lower)) {
+ return "array";
+ } else {
+ return type; // 保留自定义类型名,如 date, uri 等
+ }
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/route/RoutingAgent.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/route/RoutingAgent.java
new file mode 100644
index 0000000..4f52036
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/route/RoutingAgent.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent.route;
+
+import com.easyagents.core.agent.IAgent;
+import com.easyagents.core.model.chat.ChatModel;
+import com.easyagents.core.prompt.MemoryPrompt;
+
+/**
+ * ReActAgent 工厂接口,支持不同 Agent 的定制化创建。
+ */
+@FunctionalInterface
+public interface RoutingAgentFactory {
+ IAgent create(ChatModel chatModel, String userQuery, MemoryPrompt memoryPrompt);
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/agent/route/RoutingAgentRegistry.java b/easy-agents-core/src/main/java/com/easyagents/core/agent/route/RoutingAgentRegistry.java
new file mode 100644
index 0000000..e469b65
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/agent/route/RoutingAgentRegistry.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.agent.route;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Agent 注册中心,用于管理所有可用的 ReActAgent 工厂。
+ */
+public class RoutingAgentRegistry {
+
+ private final Map agentFactories = new HashMap<>();
+ private final Map agentDescriptions = new HashMap<>();
+ private final Map keywordToAgent = new HashMap<>();
+
+ /**
+ * 注册 Agent,并可选绑定关键字(用于快速匹配)
+ */
+ public void register(String name, String description, RoutingAgentFactory factory) {
+ register(name, description, null, factory);
+ }
+
+ public void register(String name, String description, List keywords, RoutingAgentFactory factory) {
+ agentFactories.put(name, factory);
+ agentDescriptions.put(name, description);
+
+ if (keywords != null && !keywords.isEmpty()) {
+ for (String kw : keywords) {
+ if (kw != null && !kw.trim().isEmpty()) {
+ keywordToAgent.put(kw.trim().toLowerCase(), name);
+ }
+ }
+ }
+ }
+
+ // 按关键字查找 Agent
+ public String findAgentByKeyword(String userQuery) {
+ if (userQuery == null) return null;
+ String lowerQuery = userQuery.toLowerCase();
+ for (Map.Entry entry : keywordToAgent.entrySet()) {
+ if (lowerQuery.contains(entry.getKey())) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+
+ public RoutingAgentFactory getAgentFactory(String name) {
+ return agentFactories.get(name);
+ }
+
+ public String getAgentDescriptions() {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry entry : agentDescriptions.entrySet()) {
+ sb.append("- ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
+ }
+ return sb.toString().trim();
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/BigDecimalConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/BigDecimalConverter.java
new file mode 100644
index 0000000..1c1c7c5
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/BigDecimalConverter.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class BigDecimalConverter implements IConverter {
+ @Override
+ public java.math.BigDecimal convert(String text) {
+ return new java.math.BigDecimal(text);
+ }
+}
+
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/BigIntegerConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/BigIntegerConverter.java
new file mode 100644
index 0000000..ea190e1
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/BigIntegerConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class BigIntegerConverter implements IConverter {
+ @Override
+ public java.math.BigInteger convert(String text) {
+ return new java.math.BigInteger(text);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/BooleanConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/BooleanConverter.java
new file mode 100644
index 0000000..41ed90c
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/BooleanConverter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class BooleanConverter implements IConverter {
+ @Override
+ public Boolean convert(String text) {
+ String value = text.toLowerCase();
+ if ("true".equals(value) || "1".equals(value)) {
+ return Boolean.TRUE;
+ } else if ("false".equals(value) || "0".equals(value)) {
+ return Boolean.FALSE;
+ } else {
+ throw new RuntimeException("Can not parse to boolean type of value: " + text);
+ }
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/ByteArrayConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/ByteArrayConverter.java
new file mode 100644
index 0000000..a08d69d
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/ByteArrayConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class ByteArrayConverter implements IConverter {
+ @Override
+ public byte[] convert(String text) {
+ return text.getBytes();
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/ByteConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/ByteConverter.java
new file mode 100644
index 0000000..d4217ff
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/ByteConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class ByteConverter implements IConverter {
+ @Override
+ public Byte convert(String text) {
+ return Byte.parseByte(text);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/ConvertException.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/ConvertException.java
new file mode 100644
index 0000000..efdbeae
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/ConvertException.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class ConvertException extends RuntimeException{
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/ConvertService.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/ConvertService.java
new file mode 100644
index 0000000..ccf5ba1
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/ConvertService.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+import com.easyagents.core.util.ArrayUtil;
+import com.easyagents.core.util.StringUtil;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ConvertService {
+
+ private static final Map, IConverter>> CONVERTER_MAP = new HashMap<>();
+
+ static {
+ register(new BooleanConverter(), Boolean.class, boolean.class);
+ register(new IntegerConverter(), Integer.class, int.class);
+ register(new LongConverter(), Long.class, long.class);
+ register(new DoubleConverter(), Double.class, double.class);
+ register(new FloatConverter(), Float.class, float.class);
+ register(new ShortConverter(), Short.class, short.class);
+
+ register(new BigDecimalConverter(), BigDecimal.class);
+ register(new BigIntegerConverter(), BigInteger.class);
+ register(new ByteConverter(), byte.class);
+ register(new ByteArrayConverter(), byte[].class);
+
+
+ }
+
+ private static void register(IConverter> converter, Class>... classes) {
+ for (Class> clazz : classes) {
+ CONVERTER_MAP.put(clazz, converter);
+ }
+ }
+
+ public static Object convert(Object value, Class> toType) {
+ if (value == null || (value.getClass() == String.class && StringUtil.noText((String) value)
+ && toType != String.class)) {
+ return null;
+ }
+
+ if (value.getClass().isAssignableFrom(toType)) {
+ return value;
+ }
+
+ if (toType == Serializable.class && ArrayUtil.contains(value.getClass().getInterfaces(), Serializable.class)) {
+ return value;
+ }
+
+ String valueString = value.toString().trim();
+ if (valueString.isEmpty()) {
+ return null;
+ }
+
+ IConverter> converter = CONVERTER_MAP.get(toType);
+ if (converter != null) {
+ return converter.convert(valueString);
+ }
+
+ return null;
+ }
+
+ public static Object getPrimitiveDefaultValue(Class> paraClass) {
+ if (paraClass == int.class || paraClass == long.class || paraClass == float.class || paraClass == double.class) {
+ return 0;
+ } else if (paraClass == boolean.class) {
+ return Boolean.FALSE;
+ } else if (paraClass == short.class) {
+ return (short) 0;
+ } else if (paraClass == byte.class) {
+ return (byte) 0;
+ } else if (paraClass == char.class) {
+ return '\u0000';
+ } else {
+ //不存在这种类型
+ return null;
+ }
+ }
+
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/DoubleConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/DoubleConverter.java
new file mode 100644
index 0000000..3670eb3
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/DoubleConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class DoubleConverter implements IConverter {
+ @Override
+ public Double convert(String text) {
+ return Double.parseDouble(text);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/FloatConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/FloatConverter.java
new file mode 100644
index 0000000..1499e44
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/FloatConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class FloatConverter implements IConverter {
+ @Override
+ public Float convert(String text) {
+ return Float.parseFloat(text);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/IConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/IConverter.java
new file mode 100644
index 0000000..960853f
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/IConverter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public interface IConverter {
+
+ /**
+ * convert the given text to type .
+ *
+ * @param text the text to convert.
+ * @return the convert value or null.
+ */
+ T convert(String text) throws ConvertException;
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/IntegerConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/IntegerConverter.java
new file mode 100644
index 0000000..84c3d91
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/IntegerConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class IntegerConverter implements IConverter{
+ @Override
+ public Integer convert(String text) throws ConvertException {
+ return Integer.parseInt(text);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/LongConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/LongConverter.java
new file mode 100644
index 0000000..3fd000c
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/LongConverter.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class LongConverter implements IConverter {
+ @Override
+ public Long convert(String text) {
+ return Long.parseLong(text);
+ }
+}
+
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/convert/ShortConverter.java b/easy-agents-core/src/main/java/com/easyagents/core/convert/ShortConverter.java
new file mode 100644
index 0000000..b0f9454
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/convert/ShortConverter.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.convert;
+
+public class ShortConverter implements IConverter{
+ @Override
+ public Short convert(String text) throws ConvertException {
+ return Short.parseShort(text);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/Document.java b/easy-agents-core/src/main/java/com/easyagents/core/document/Document.java
new file mode 100644
index 0000000..0b36b59
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/Document.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document;
+
+import com.easyagents.core.store.VectorData;
+
+public class Document extends VectorData {
+
+ /**
+ * Document ID
+ */
+ private Object id;
+
+ /**
+ * Document title
+ */
+ private String title;
+
+ /**
+ * Document Content
+ */
+ private String content;
+
+
+ /**
+ * 得分,目前只有在 rerank 场景使用
+ */
+ private Double score;
+
+
+ public Document() {
+ }
+
+ public Document(String content) {
+ this.content = content;
+ }
+
+ public Object getId() {
+ return id;
+ }
+
+ public void setId(Object id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public Double getScore() {
+ return score;
+ }
+
+ @Override
+ public void setScore(Double score) {
+ this.score = score;
+ }
+
+ public static Document of(String content){
+ Document document = new Document();
+ document.setContent(content);
+ return document;
+ }
+
+ @Override
+ public String toString() {
+ return "Document{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", content='" + content + '\'' +
+ ", score=" + score +
+ '}';
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/DocumentSplitter.java b/easy-agents-core/src/main/java/com/easyagents/core/document/DocumentSplitter.java
new file mode 100644
index 0000000..366ab1c
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/DocumentSplitter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document;
+
+import com.easyagents.core.document.id.DocumentIdGenerator;
+
+import java.util.Collections;
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
+
+public interface DocumentSplitter {
+
+ List split(Document text, DocumentIdGenerator idGenerator);
+
+ default List split(Document text) {
+ return split(text, null);
+ }
+
+ default List splitAll(List documents, DocumentIdGenerator idGenerator) {
+ if (documents == null || documents.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return documents.stream()
+ .flatMap(document -> split(document, idGenerator).stream())
+ .collect(toList());
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/id/DocumentIdGenerator.java b/easy-agents-core/src/main/java/com/easyagents/core/document/id/DocumentIdGenerator.java
new file mode 100644
index 0000000..1bfb972
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/id/DocumentIdGenerator.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.id;
+
+import com.easyagents.core.document.Document;
+
+public interface DocumentIdGenerator {
+
+ /**
+ * Generate a unique ID for the Document
+ *
+ * @param document Document
+ * @return the unique ID
+ */
+ Object generateId(Document document);
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/id/DocumentIdGeneratorFactory.java b/easy-agents-core/src/main/java/com/easyagents/core/document/id/DocumentIdGeneratorFactory.java
new file mode 100644
index 0000000..cbd9b84
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/id/DocumentIdGeneratorFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.id;
+
+public abstract class DocumentIdGeneratorFactory {
+
+ private static DocumentIdGeneratorFactory factory = new DocumentIdGeneratorFactory() {
+ final MD5IdGenerator randomIdGenerator = new MD5IdGenerator();
+
+ @Override
+ public DocumentIdGenerator createGenerator() {
+ return randomIdGenerator;
+ }
+ };
+
+ public static DocumentIdGeneratorFactory getFactory() {
+ return factory;
+ }
+
+ public static void setFactory(DocumentIdGeneratorFactory factory) {
+ if (factory == null) {
+ throw new NullPointerException("factory can not be null");
+ }
+ DocumentIdGeneratorFactory.factory = factory;
+ }
+
+ public static DocumentIdGenerator getDocumentIdGenerator() {
+ return factory.createGenerator();
+ }
+
+
+ abstract DocumentIdGenerator createGenerator();
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/id/MD5IdGenerator.java b/easy-agents-core/src/main/java/com/easyagents/core/document/id/MD5IdGenerator.java
new file mode 100644
index 0000000..fd8de10
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/id/MD5IdGenerator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.id;
+
+import com.easyagents.core.document.Document;
+import com.easyagents.core.util.HashUtil;
+
+public class MD5IdGenerator implements DocumentIdGenerator {
+ /**
+ * Generate a unique ID for the Document
+ *
+ * @param document Document
+ * @return the unique ID
+ */
+ @Override
+ public Object generateId(Document document) {
+ return document.getContent() != null ? HashUtil.md5(document.getContent()) : null;
+ }
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/id/RandomIdGenerator.java b/easy-agents-core/src/main/java/com/easyagents/core/document/id/RandomIdGenerator.java
new file mode 100644
index 0000000..b37dae4
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/id/RandomIdGenerator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.id;
+
+import com.easyagents.core.document.Document;
+
+import java.util.UUID;
+
+public class RandomIdGenerator implements DocumentIdGenerator {
+
+ /**
+ * Generate a unique ID for the Document
+ *
+ * @param document Document
+ * @return the unique ID
+ */
+ @Override
+ public Object generateId(Document document) {
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/AIDocumentSplitter.java b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/AIDocumentSplitter.java
new file mode 100644
index 0000000..fcd4a14
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/AIDocumentSplitter.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.splitter;
+
+import com.easyagents.core.document.Document;
+import com.easyagents.core.document.DocumentSplitter;
+import com.easyagents.core.document.id.DocumentIdGenerator;
+import com.easyagents.core.model.chat.ChatModel;
+import com.easyagents.core.model.chat.ChatOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * AIDocumentSplitter:基于大模型(AI/LLM)的语义文档拆分器。
+ * 使用 "---" 作为段落分隔符,避免 JSON 解析风险。
+ * 支持注入 fallback 拆分器以提高鲁棒性。
+ */
+public class AIDocumentSplitter implements DocumentSplitter {
+
+ private static final Logger log = LoggerFactory.getLogger(AIDocumentSplitter.class);
+
+ private static final String DEFAULT_SPLIT_PROMPT_TEMPLATE =
+ "你是一个专业的文档处理助手,请将以下长文档按语义拆分为多个逻辑连贯的段落块。\n" +
+ "要求:\n" +
+ "1. 每个块应保持主题/语义完整性,避免在句子中间切断。\n" +
+ "2. 每个块长度建议在 200-500 字之间(可根据内容灵活调整)。\n" +
+ "3. **不要添加任何解释、编号、前缀或后缀**。\n" +
+ "4. **仅用三连短横线 \"---\" 作为块之间的分隔符**,格式如下:\n" +
+ "\n" +
+ "块1内容\n" +
+ "---\n" +
+ "块2内容\n" +
+ "---\n" +
+ "块3内容\n" +
+ "\n" +
+ "注意:开头不要有 ---,结尾也不要有多余的 ---。\n" +
+ "\n" +
+ "文档内容如下:\n" +
+ "{document}";
+
+ private static final String CHUNK_SEPARATOR = "---";
+
+ private final ChatModel chatModel;
+ private String splitPromptTemplate = DEFAULT_SPLIT_PROMPT_TEMPLATE;
+ private ChatOptions chatOptions = new ChatOptions.Builder().temperature(0.2f).build();
+ private int maxChunks = 20;
+ private int maxTotalLength = 10000;
+
+ // 可配置的 fallback 拆分器
+ private DocumentSplitter fallbackSplitter;
+
+ public AIDocumentSplitter(ChatModel chatModel) {
+ this.chatModel = chatModel;
+ }
+
+ @Override
+ public List split(Document document, DocumentIdGenerator idGenerator) {
+ if (document == null || document.getContent() == null || document.getContent().trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ String content = document.getContent().trim();
+ if (content.length() > maxTotalLength) {
+ log.warn("文档过长({} 字符),已截断至 {} 字符", content.length(), maxTotalLength);
+ content = content.substring(0, maxTotalLength);
+ }
+
+ List chunks;
+ try {
+ String prompt = splitPromptTemplate.replace("{document}", content);
+ String llmOutput = chatModel.chat(prompt, chatOptions);
+
+ chunks = parseChunksBySeparator(llmOutput, CHUNK_SEPARATOR);
+ } catch (Exception e) {
+ log.error("AI 拆分失败,使用 fallback 拆分器", e);
+ if (fallbackSplitter == null) {
+ log.error("没有可用的 fallback 拆分器,请检查配置");
+ return Collections.emptyList();
+ }
+ List fallbackDocs = fallbackSplitter.split(document, idGenerator);
+ if (fallbackDocs.size() > maxChunks) {
+ return new ArrayList<>(fallbackDocs.subList(0, maxChunks));
+ }
+ return fallbackDocs;
+ }
+
+ List validChunks = chunks.stream()
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .limit(maxChunks)
+ .collect(Collectors.toList());
+
+ List result = new ArrayList<>();
+ for (String chunk : validChunks) {
+ Document doc = new Document();
+ doc.setContent(chunk);
+ doc.setTitle(document.getTitle());
+ if (idGenerator != null) {
+ doc.setId(idGenerator.generateId(doc));
+ }
+ result.add(doc);
+ }
+
+ return result;
+ }
+
+ private List parseChunksBySeparator(String text, String separator) {
+ if (text == null || text.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ String[] parts = text.split(separator, -1);
+ List chunks = new ArrayList<>();
+ for (String part : parts) {
+ String trimmed = part.trim();
+ if (!trimmed.isEmpty()) {
+ chunks.add(trimmed);
+ }
+ }
+
+ if (chunks.size() == 1 && text.contains(separator)) {
+ return tryAlternativeSplit(text, separator);
+ }
+
+ return chunks;
+ }
+
+ private List tryAlternativeSplit(String text, String separator) {
+ String normalized = text.replaceAll("\\s*---\\s*", "---");
+ return parseChunksBySeparator(normalized, separator);
+ }
+
+ // ===== Getters & Setters =====
+
+ public void setFallbackSplitter(DocumentSplitter fallbackSplitter) {
+ this.fallbackSplitter = fallbackSplitter;
+ }
+
+ public void setSplitPromptTemplate(String splitPromptTemplate) {
+ if (splitPromptTemplate != null && !splitPromptTemplate.trim().isEmpty()) {
+ this.splitPromptTemplate = splitPromptTemplate;
+ }
+ }
+
+ public void setChatOptions(ChatOptions chatOptions) {
+ if (chatOptions != null) {
+ this.chatOptions = chatOptions;
+ }
+ }
+
+ public void setMaxChunks(int maxChunks) {
+ this.maxChunks = Math.max(1, maxChunks);
+ }
+
+ public void setMaxTotalLength(int maxTotalLength) {
+ this.maxTotalLength = Math.max(100, maxTotalLength);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/MarkdownHeaderSplitter.java b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/MarkdownHeaderSplitter.java
new file mode 100644
index 0000000..1a34b5c
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/MarkdownHeaderSplitter.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.splitter;
+
+import com.easyagents.core.document.Document;
+import com.easyagents.core.document.DocumentSplitter;
+import com.easyagents.core.document.id.DocumentIdGenerator;
+import com.easyagents.core.util.StringUtil;
+
+import java.util.*;
+
+public class MarkdownHeaderSplitter implements DocumentSplitter {
+
+ /**
+ * 最大标题级别(inclusive),用于触发拆分。
+ * 例如:splitLevel = 2 表示在 # 和 ## 处拆分,### 及以下不作为新块起点。
+ */
+ private int splitLevel;
+
+ /**
+ * 是否在每个 chunk 中保留父级标题路径(如 "Introduction > Background")
+ */
+ private boolean includeParentHeaders = true;
+
+ public MarkdownHeaderSplitter() {
+ }
+
+ public MarkdownHeaderSplitter(int splitLevel) {
+ if (splitLevel < 1 || splitLevel > 6) {
+ throw new IllegalArgumentException("splitLevel must be between 1 and 6, got: " + splitLevel);
+ }
+ this.splitLevel = splitLevel;
+ }
+
+ public MarkdownHeaderSplitter(int splitLevel, boolean includeParentHeaders) {
+ this(splitLevel);
+ this.includeParentHeaders = includeParentHeaders;
+ }
+
+ public int getSplitLevel() {
+ return splitLevel;
+ }
+
+ public void setSplitLevel(int splitLevel) {
+ if (splitLevel < 1 || splitLevel > 6) {
+ throw new IllegalArgumentException("splitLevel must be between 1 and 6");
+ }
+ this.splitLevel = splitLevel;
+ }
+
+ public boolean isIncludeParentHeaders() {
+ return includeParentHeaders;
+ }
+
+ public void setIncludeParentHeaders(boolean includeParentHeaders) {
+ this.includeParentHeaders = includeParentHeaders;
+ }
+
+ @Override
+ public List split(Document document, DocumentIdGenerator idGenerator) {
+ if (document == null || StringUtil.noText(document.getContent())) {
+ return Collections.emptyList();
+ }
+
+ String content = document.getContent();
+ String[] lines = content.split("\n");
+
+ List chunks = new ArrayList<>();
+ Deque headerStack = new ArrayDeque<>();
+
+ StringBuilder currentContent = new StringBuilder();
+ int currentStartLine = 0;
+
+ boolean inCodeBlock = false;
+
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ if (line == null) {
+ currentContent.append("\n");
+ continue;
+ }
+
+ // 检测围栏代码块的开始或结束(支持 ``` 或 ~~~)
+ String trimmedLine = stripLeading(line);
+ if (trimmedLine.startsWith("```") || trimmedLine.startsWith("~~~")) {
+ inCodeBlock = !inCodeBlock;
+ currentContent.append(line).append("\n");
+ continue;
+ }
+
+ if (!inCodeBlock) {
+ HeaderInfo header = parseHeader(line);
+ if (header != null && header.level <= splitLevel) {
+ // 触发新 chunk
+ if (currentContent.length() > 0 || !chunks.isEmpty()) {
+ flushChunk(chunks, currentContent.toString(), headerStack, currentStartLine, i - 1, document);
+ currentContent.setLength(0);
+ }
+ currentStartLine = i;
+
+ // 弹出栈中层级大于等于当前的标题
+ while (!headerStack.isEmpty() && headerStack.peek().level >= header.level) {
+ headerStack.pop();
+ }
+ headerStack.push(header);
+
+ // 将标题行加入当前内容(保留结构)
+ currentContent.append(line).append("\n");
+ continue;
+ }
+ }
+
+ // 普通文本行或代码块内行
+ currentContent.append(line).append("\n");
+ }
+
+ // Flush remaining content
+ if (currentContent.length() > 0) {
+ flushChunk(chunks, currentContent.toString(), headerStack, currentStartLine, lines.length - 1, document);
+ }
+
+ // 构建结果 Document 列表
+ List result = new ArrayList<>();
+ for (DocumentChunk chunk : chunks) {
+ Document doc = new Document();
+ doc.setContent(chunk.content.trim());
+ doc.addMetadata(document.getMetadataMap());
+
+ if (includeParentHeaders && !chunk.headerPath.isEmpty()) {
+ doc.addMetadata("header_path", String.join(" > ", chunk.headerPath));
+ }
+ doc.addMetadata("start_line", String.valueOf(chunk.startLine));
+ doc.addMetadata("end_line", String.valueOf(chunk.endLine));
+
+ if (idGenerator != null) {
+ doc.setId(idGenerator.generateId(doc));
+ }
+ result.add(doc);
+ }
+
+ return result;
+ }
+
+ private void flushChunk(List chunks, String content,
+ Deque headerStack, int startLine, int endLine, Document sourceDoc) {
+ if (StringUtil.noText(content.trim())) {
+ return;
+ }
+
+ // 从根到当前构建标题路径
+ List headerPath = new ArrayList<>();
+ List stackCopy = new ArrayList<>(headerStack);
+ Collections.reverse(stackCopy);
+ for (HeaderInfo h : stackCopy) {
+ headerPath.add(h.text);
+ }
+
+ chunks.add(new DocumentChunk(content, headerPath, startLine, endLine));
+ }
+
+ /**
+ * 解析一行是否为合法的 ATX 标题(# Title)
+ *
+ * @param line 输入行
+ * @return HeaderInfo 或 null
+ */
+ private HeaderInfo parseHeader(String line) {
+ if (line == null || line.isEmpty()) {
+ return null;
+ }
+ line = stripLeading(line);
+ if (!line.startsWith("#")) {
+ return null;
+ }
+
+ int level = 0;
+ int i = 0;
+ while (i < line.length() && line.charAt(i) == '#') {
+ level++;
+ i++;
+ }
+
+ if (level > 6) {
+ return null; // 非法标题
+ }
+
+ // 必须后跟空格或行结束(符合 CommonMark 规范)
+ if (i < line.length() && line.charAt(i) != ' ') {
+ return null;
+ }
+
+ String text = line.substring(i).trim();
+ return new HeaderInfo(level, text);
+ }
+
+ private static String stripLeading(String s) {
+ if (s == null || s.isEmpty()) {
+ return s;
+ }
+ int i = 0;
+ while (i < s.length() && Character.isWhitespace(s.charAt(i))) {
+ i++;
+ }
+ return i == 0 ? s : s.substring(i);
+ }
+
+ // -- 内部辅助类 --
+
+ private static class HeaderInfo {
+ final int level;
+ final String text;
+
+ HeaderInfo(int level, String text) {
+ this.level = level;
+ this.text = text;
+ }
+ }
+
+ private static class DocumentChunk {
+ final String content;
+ final List headerPath;
+ final int startLine;
+ final int endLine;
+
+ DocumentChunk(String content, List headerPath, int startLine, int endLine) {
+ this.content = content;
+ this.headerPath = headerPath;
+ this.startLine = startLine;
+ this.endLine = endLine;
+ }
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/RegexDocumentSplitter.java b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/RegexDocumentSplitter.java
new file mode 100644
index 0000000..c6fd94d
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/RegexDocumentSplitter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.splitter;
+
+import com.easyagents.core.document.DocumentSplitter;
+import com.easyagents.core.document.Document;
+import com.easyagents.core.document.id.DocumentIdGenerator;
+import com.easyagents.core.util.StringUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class RegexDocumentSplitter implements DocumentSplitter {
+
+ private final String regex;
+
+ public RegexDocumentSplitter(String regex) {
+ this.regex = regex;
+ }
+
+ @Override
+ public List split(Document document, DocumentIdGenerator idGenerator) {
+ if (document == null || StringUtil.noText(document.getContent())) {
+ return Collections.emptyList();
+ }
+ String[] textArray = document.getContent().split(regex);
+ List chunks = new ArrayList<>(textArray.length);
+ for (String textString : textArray) {
+ if (StringUtil.noText(textString)) {
+ continue;
+ }
+ Document newDocument = new Document();
+ newDocument.addMetadata(document.getMetadataMap());
+ newDocument.setContent(textString);
+
+ //we should invoke setId after setContent
+ newDocument.setId(idGenerator == null ? null : idGenerator.generateId(newDocument));
+ chunks.add(newDocument);
+ }
+ return chunks;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/SimpleDocumentSplitter.java b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/SimpleDocumentSplitter.java
new file mode 100644
index 0000000..65910ac
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/SimpleDocumentSplitter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.splitter;
+
+import com.easyagents.core.document.Document;
+import com.easyagents.core.document.DocumentSplitter;
+import com.easyagents.core.document.id.DocumentIdGenerator;
+import com.easyagents.core.util.StringUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SimpleDocumentSplitter implements DocumentSplitter {
+ private int chunkSize;
+ private int overlapSize;
+
+ public SimpleDocumentSplitter(int chunkSize) {
+ this.chunkSize = chunkSize;
+ if (this.chunkSize <= 0) {
+ throw new IllegalArgumentException("chunkSize must be greater than 0, chunkSize: " + this.chunkSize);
+ }
+ }
+
+ public SimpleDocumentSplitter(int chunkSize, int overlapSize) {
+ this.chunkSize = chunkSize;
+ this.overlapSize = overlapSize;
+
+ if (this.chunkSize <= 0) {
+ throw new IllegalArgumentException("chunkSize must be greater than 0, chunkSize: " + this.chunkSize);
+ }
+ if (this.overlapSize >= this.chunkSize) {
+ throw new IllegalArgumentException("overlapSize must be less than chunkSize, overlapSize: " + this.overlapSize + ", chunkSize: " + this.chunkSize);
+ }
+ }
+
+ public int getChunkSize() {
+ return chunkSize;
+ }
+
+ public void setChunkSize(int chunkSize) {
+ this.chunkSize = chunkSize;
+ }
+
+ public int getOverlapSize() {
+ return overlapSize;
+ }
+
+ public void setOverlapSize(int overlapSize) {
+ this.overlapSize = overlapSize;
+ }
+
+ @Override
+ public List split(Document document, DocumentIdGenerator idGenerator) {
+ if (document == null || StringUtil.noText(document.getContent())) {
+ return Collections.emptyList();
+ }
+
+ String content = document.getContent();
+ int index = 0, currentIndex = index;
+ int maxIndex = content.length();
+
+ List chunks = new ArrayList<>();
+ while (currentIndex < maxIndex) {
+ int endIndex = Math.min(currentIndex + chunkSize, maxIndex);
+ String chunk = content.substring(currentIndex, endIndex).trim();
+ currentIndex = currentIndex + chunkSize - overlapSize;
+
+ if (chunk.isEmpty()) {
+ continue;
+ }
+
+ Document newDocument = new Document();
+ newDocument.addMetadata(document.getMetadataMap());
+ newDocument.setContent(chunk);
+
+ //we should invoke setId after setContent
+ newDocument.setId(idGenerator == null ? null : idGenerator.generateId(newDocument));
+ chunks.add(newDocument);
+ }
+
+ return chunks;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/SimpleTokenizeSplitter.java b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/SimpleTokenizeSplitter.java
new file mode 100644
index 0000000..e7cc454
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/document/splitter/SimpleTokenizeSplitter.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.document.splitter;
+
+import com.easyagents.core.document.Document;
+import com.easyagents.core.document.DocumentSplitter;
+import com.easyagents.core.document.id.DocumentIdGenerator;
+import com.easyagents.core.util.StringUtil;
+import com.knuddels.jtokkit.Encodings;
+import com.knuddels.jtokkit.api.Encoding;
+import com.knuddels.jtokkit.api.EncodingRegistry;
+import com.knuddels.jtokkit.api.EncodingType;
+import com.knuddels.jtokkit.api.IntArrayList;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SimpleTokenizeSplitter implements DocumentSplitter {
+ private EncodingRegistry registry = Encodings.newLazyEncodingRegistry();
+ private EncodingType encodingType = EncodingType.CL100K_BASE;
+ private int chunkSize;
+ private int overlapSize;
+
+ public SimpleTokenizeSplitter(int chunkSize) {
+ this.chunkSize = chunkSize;
+ if (this.chunkSize <= 0) {
+ throw new IllegalArgumentException("chunkSize must be greater than 0, chunkSize: " + this.chunkSize);
+ }
+ }
+
+ public SimpleTokenizeSplitter(int chunkSize, int overlapSize) {
+ this.chunkSize = chunkSize;
+ this.overlapSize = overlapSize;
+
+ if (this.chunkSize <= 0) {
+ throw new IllegalArgumentException("chunkSize must be greater than 0, chunkSize: " + this.chunkSize);
+ }
+ if (this.overlapSize >= this.chunkSize) {
+ throw new IllegalArgumentException("overlapSize must be less than chunkSize, overlapSize: " + this.overlapSize + ", chunkSize: " + this.chunkSize);
+ }
+ }
+
+ public int getChunkSize() {
+ return chunkSize;
+ }
+
+ public void setChunkSize(int chunkSize) {
+ this.chunkSize = chunkSize;
+ }
+
+ public int getOverlapSize() {
+ return overlapSize;
+ }
+
+ public void setOverlapSize(int overlapSize) {
+ this.overlapSize = overlapSize;
+ }
+
+ public EncodingRegistry getRegistry() {
+ return registry;
+ }
+
+ public void setRegistry(EncodingRegistry registry) {
+ this.registry = registry;
+ }
+
+ public EncodingType getEncodingType() {
+ return encodingType;
+ }
+
+ public void setEncodingType(EncodingType encodingType) {
+ this.encodingType = encodingType;
+ }
+
+ @Override
+ public List split(Document document, DocumentIdGenerator idGenerator) {
+ if (document == null || StringUtil.noText(document.getContent())) {
+ return Collections.emptyList();
+ }
+
+ String content = document.getContent();
+ Encoding encoding = this.registry.getEncoding(this.encodingType);
+
+ List tokens = encoding.encode(content).boxed();
+
+
+ int index = 0, currentIndex = index;
+ int maxIndex = tokens.size();
+
+ List chunks = new ArrayList<>();
+ while (currentIndex < maxIndex) {
+ int endIndex = Math.min(currentIndex + chunkSize, maxIndex);
+ List chunkTokens = tokens.subList(currentIndex, endIndex);
+
+ IntArrayList intArrayList = new IntArrayList();
+ for (Integer chunkToken : chunkTokens) {
+ intArrayList.add(chunkToken);
+ }
+ String chunkText = encoding.decode(intArrayList).trim();
+ if (chunkText.isEmpty()) {
+ continue;
+ }
+
+ //UTF-8 'Unicode replacement character' which in your case is 0xFFFD (65533 in Hex).
+ //fix 修复中文乱码的问题
+ boolean firstIsReplacement = chunkText.charAt(0) == 65533;
+ boolean lastIsReplacement = chunkText.charAt(chunkText.length() - 1) == 65533;
+
+ if (firstIsReplacement || lastIsReplacement) {
+ if (firstIsReplacement) currentIndex -= 1;
+ if (lastIsReplacement) endIndex += 1;
+
+ chunkTokens = tokens.subList(currentIndex, endIndex);
+ intArrayList = new IntArrayList();
+ for (Integer chunkToken : chunkTokens) {
+ intArrayList.add(chunkToken);
+ }
+
+ chunkText = encoding.decode(intArrayList).trim();
+ }
+
+ currentIndex = currentIndex + chunkSize - overlapSize;
+
+ Document newDocument = new Document();
+ newDocument.addMetadata(document.getMetadataMap());
+ newDocument.setContent(chunkText);
+
+ //we should invoke setId after setContent
+ newDocument.setId(idGenerator == null ? null : idGenerator.generateId(newDocument));
+ chunks.add(newDocument);
+ }
+
+ return chunks;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/File2TextService.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/File2TextService.java
new file mode 100644
index 0000000..281a1ae
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/File2TextService.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text;
+
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.extractor.ExtractorRegistry;
+import com.easyagents.core.file2text.source.*;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class File2TextService {
+ private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(File2TextService.class);
+ private final ExtractorRegistry registry;
+
+ public File2TextService() {
+ this(new ExtractorRegistry());
+ }
+
+ public File2TextService(ExtractorRegistry registry) {
+ this.registry = registry;
+ }
+
+ public ExtractorRegistry getRegistry() {
+ return registry;
+ }
+
+ public String extractTextFromHttpUrl(String httpUrl) {
+ return extractTextFromSource(new HttpDocumentSource(httpUrl));
+ }
+
+ public String extractTextFromHttpUrl(String httpUrl, String fileName) {
+ return extractTextFromSource(new HttpDocumentSource(httpUrl, fileName));
+ }
+
+ public String extractTextFromHttpUrl(String httpUrl, String fileName, String mimeType) {
+ return extractTextFromSource(new HttpDocumentSource(httpUrl, fileName, mimeType));
+ }
+
+ public String extractTextFromFile(File file) {
+ return extractTextFromSource(new FileDocumentSource(file));
+ }
+
+ public String extractTextFromStream(InputStream is, String fileName, String mimeType) {
+ return extractTextFromSource(new ByteStreamDocumentSource(is, fileName, mimeType));
+ }
+
+ public String extractTextFromBytes(byte[] bytes, String fileName, String mimeType) {
+ return extractTextFromSource(new ByteArrayDocumentSource(bytes, fileName, mimeType));
+ }
+
+
+ /**
+ * 从 DocumentSource 提取纯文本
+ * 支持多 Extractor 降级重试
+ *
+ * @param source 文档输入源
+ * @return 提取的文本(非空去空格),若无法提取则抛出异常
+ * @throws IllegalArgumentException 输入源为空
+ */
+ public String extractTextFromSource(DocumentSource source) {
+ if (source == null) {
+ throw new IllegalArgumentException("DocumentSource cannot be null");
+ }
+
+ try {
+ // 获取可用的 Extractor(按优先级排序)
+ List candidates = registry.findExtractors(source);
+ if (candidates.isEmpty()) {
+ log.warn("No extractor supports this document: " + safeFileName(source));
+ return null;
+ }
+
+ // 日志:输出候选 Extractor
+ log.info("Trying extractors for {}: {}", safeFileName(source),
+ candidates.stream()
+ .map(e -> e.getClass().getSimpleName())
+ .collect(Collectors.joining(", ")));
+
+
+ for (FileExtractor extractor : candidates) {
+ try {
+ log.debug("Trying {} on {}", extractor.getClass().getSimpleName(), safeFileName(source));
+
+ String text = extractor.extractText(source);
+ if (text != null && !text.trim().isEmpty()) {
+ log.debug("Success with {}: extracted {} chars",
+ extractor.getClass().getSimpleName(), text.length());
+ return text;
+ } else {
+ log.debug("Extractor {} returned null", extractor.getClass().getSimpleName());
+ }
+ } catch (Exception e) {
+ log.warn("Extractor {} failed on {}: {}",
+ extractor.getClass().getSimpleName(),
+ safeFileName(source),
+ e.toString());
+ }
+ }
+
+ log.warn(String.format("All %d extractors failed for: %s", candidates.size(), safeFileName(source)));
+ return null;
+ } finally {
+ source.cleanup();
+ }
+
+ }
+
+ private String safeFileName(DocumentSource source) {
+ try {
+ return source.getFileName() != null ? source.getFileName() : "unknown";
+ } catch (Exception e) {
+ return "unknown";
+ }
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/File2TextUtil.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/File2TextUtil.java
new file mode 100644
index 0000000..392cbb2
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/File2TextUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text;
+
+
+import com.easyagents.core.file2text.source.ByteArrayDocumentSource;
+import com.easyagents.core.file2text.source.ByteStreamDocumentSource;
+import com.easyagents.core.file2text.source.FileDocumentSource;
+import com.easyagents.core.file2text.source.HttpDocumentSource;
+
+import java.io.File;
+import java.io.InputStream;
+
+public class File2TextUtil {
+ private static File2TextService file2TextService = new File2TextService();
+
+ public static void setFile2TextService(File2TextService file2TextService) {
+ if (file2TextService == null) {
+ throw new IllegalArgumentException("File2TextService cannot be null");
+ }
+ File2TextUtil.file2TextService = file2TextService;
+ }
+
+ public static String readFromHttpUrl(String httpUrl) {
+ return file2TextService.extractTextFromSource(new HttpDocumentSource(httpUrl));
+ }
+
+ public static String readFromHttpUrl(String httpUrl, String fileName) {
+ return file2TextService.extractTextFromSource(new HttpDocumentSource(httpUrl, fileName));
+ }
+
+ public static String readFromHttpUrl(String httpUrl, String fileName, String mimeType) {
+ return file2TextService.extractTextFromSource(new HttpDocumentSource(httpUrl, fileName, mimeType));
+ }
+
+ public static String readFromFile(File file) {
+ return file2TextService.extractTextFromSource(new FileDocumentSource(file));
+ }
+
+ public static String readFromStream(InputStream is, String fileName, String mimeType) {
+ return file2TextService.extractTextFromSource(new ByteStreamDocumentSource(is, fileName, mimeType));
+ }
+
+ public static String readFromBytes(byte[] bytes, String fileName, String mimeType) {
+ return file2TextService.extractTextFromSource(new ByteArrayDocumentSource(bytes, fileName, mimeType));
+ }
+
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/ExtractorRegistry.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/ExtractorRegistry.java
new file mode 100644
index 0000000..a2a905a
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/ExtractorRegistry.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor;
+
+
+import com.easyagents.core.file2text.extractor.impl.*;
+import com.easyagents.core.file2text.source.DocumentSource;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Extractor 注册中心
+ */
+public class ExtractorRegistry {
+
+ private final List extractors = new ArrayList<>();
+
+ public ExtractorRegistry() {
+ register(new PdfTextExtractor());
+ register(new DocxExtractor());
+ register(new DocExtractor());
+ register(new PptxExtractor());
+ register(new HtmlExtractor());
+ register(new PlainTextExtractor());
+ }
+
+ /**
+ * 注册一个 Extractor
+ */
+ public synchronized void register(FileExtractor extractor) {
+ Objects.requireNonNull(extractor, "Extractor cannot be null");
+ extractors.add(extractor);
+ }
+
+ /**
+ * 批量注册
+ */
+ public void registerAll(List extractors) {
+ extractors.forEach(this::register);
+ }
+
+
+ public List findExtractors(DocumentSource source) {
+ return extractors.stream()
+ .filter(extractor -> extractor.supports(source))
+ .sorted(FileExtractor.ORDER_COMPARATOR)
+ .collect(Collectors.toList());
+ }
+
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/FileExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/FileExtractor.java
new file mode 100644
index 0000000..62d50ca
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/FileExtractor.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor;
+
+import com.easyagents.core.file2text.source.DocumentSource;
+
+import java.io.IOException;
+import java.util.Comparator;
+
+public interface FileExtractor {
+
+ Comparator ORDER_COMPARATOR =
+ Comparator.comparingInt(FileExtractor::getOrder);
+
+ /**
+ * 判断该 Extractor 是否支持处理此文档
+ */
+ boolean supports(DocumentSource source);
+
+ String extractText(DocumentSource source) throws IOException;
+
+
+ default int getOrder() {
+ return 100;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/DocExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/DocExtractor.java
new file mode 100644
index 0000000..46f8d22
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/DocExtractor.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor.impl;
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.source.DocumentSource;
+import org.apache.poi.hwpf.HWPFDocument;
+import org.apache.poi.hwpf.extractor.WordExtractor;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * DOC 文档提取器(.doc)
+ * 支持旧版 Word 97-2003 格式
+ */
+public class DocExtractor implements FileExtractor {
+
+ private static final Set SUPPORTED_MIME_TYPES;
+ private static final Set SUPPORTED_EXTENSIONS;
+
+ static {
+ Set mimeTypes = new HashSet<>();
+ mimeTypes.add("application/msword");
+ SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
+
+ Set extensions = new HashSet<>();
+ extensions.add("doc");
+ extensions.add("dot");
+ SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
+ }
+
+ @Override
+ public boolean supports(DocumentSource source) {
+ String mimeType = source.getMimeType();
+ String fileName = source.getFileName();
+
+ if (mimeType != null && SUPPORTED_MIME_TYPES.contains(mimeType)) {
+ return true;
+ }
+
+ if (fileName != null) {
+ String ext = getExtension(fileName);
+ if (ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String extractText(DocumentSource source) throws IOException {
+ try (InputStream is = source.openStream();
+ POIFSFileSystem fs = new POIFSFileSystem(is);
+ HWPFDocument doc = new HWPFDocument(fs)) {
+
+ WordExtractor extractor = new WordExtractor(doc);
+ String[] paragraphs = extractor.getParagraphText();
+
+ StringBuilder text = new StringBuilder();
+ for (String para : paragraphs) {
+ // 清理控制字符
+ String clean = para.replaceAll("[\\r\\001]+", "").trim();
+ if (!clean.isEmpty()) {
+ text.append(clean).append("\n");
+ }
+ }
+
+ return text.toString().trim();
+ } catch (Exception e) {
+ throw new IOException("Failed to extract .doc file: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public int getOrder() {
+ return 15; // 低于 .docx
+ }
+
+ private String getExtension(String fileName) {
+ if (fileName == null || !fileName.contains(".")) return null;
+ int lastDot = fileName.lastIndexOf('.');
+ return fileName.substring(lastDot + 1);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/DocxExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/DocxExtractor.java
new file mode 100644
index 0000000..3f8dc4d
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/DocxExtractor.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor.impl;
+
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.source.DocumentSource;
+import org.apache.poi.xwpf.usermodel.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * DOCX 文档提取器(.docx, .dotx)
+ * 支持段落、表格、列表文本提取
+ */
+public class DocxExtractor implements FileExtractor {
+
+ private static final Set KNOWN_MIME_TYPES;
+ private static final String MIME_PREFIX = "application/vnd.openxmlformats-officedocument.wordprocessingml";
+ private static final Set SUPPORTED_EXTENSIONS;
+
+ static {
+ // 精确 MIME(可选)
+ Set mimeTypes = new HashSet<>();
+ mimeTypes.add("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+ KNOWN_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
+
+ // 支持的扩展名
+ Set extensions = new HashSet<>();
+ extensions.add("docx");
+ extensions.add("dotx");
+ SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
+ }
+
+ @Override
+ public boolean supports(DocumentSource source) {
+ String mimeType = source.getMimeType();
+ String fileName = source.getFileName();
+
+ // 1. MIME 精确匹配
+ if (mimeType != null && KNOWN_MIME_TYPES.contains(mimeType)) {
+ return true;
+ }
+
+ // 2. MIME 前缀匹配
+ if (mimeType != null && mimeType.startsWith(MIME_PREFIX)) {
+ return true;
+ }
+
+ // 3. 扩展名匹配
+ if (fileName != null) {
+ String ext = getExtension(fileName);
+ if (ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String extractText(DocumentSource source) throws IOException {
+ StringBuilder text = new StringBuilder();
+
+ try (InputStream is = source.openStream();
+ XWPFDocument document = new XWPFDocument(is)) {
+
+ // 提取段落
+ for (XWPFParagraph paragraph : document.getParagraphs()) {
+ String paraText = getParagraphText(paragraph);
+ if (paraText != null && !paraText.trim().isEmpty()) {
+ text.append(paraText).append("\n");
+ }
+ }
+
+ // 提取表格
+ for (XWPFTable table : document.getTables()) {
+ text.append("\n[Table Start]\n");
+ for (XWPFTableRow row : table.getRows()) {
+ List cellTexts = row.getTableCells().stream()
+ .map(this::getCellText)
+ .map(String::trim)
+ .collect(Collectors.toList());
+ text.append(cellTexts).append("\n");
+ }
+ text.append("[Table End]\n\n");
+ }
+
+ } catch (Exception e) {
+ throw new IOException("Failed to extract DOCX: " + e.getMessage(), e);
+ }
+
+ return text.toString().trim();
+ }
+
+ private String getParagraphText(XWPFParagraph paragraph) {
+ StringBuilder text = new StringBuilder();
+ for (XWPFRun run : paragraph.getRuns()) {
+ String runText = run.text();
+ if (runText != null) {
+ text.append(runText);
+ }
+ }
+ return text.length() > 0 ? text.toString() : null;
+ }
+
+ private String getCellText(XWPFTableCell cell) {
+ String simpleText = cell.getText();
+ if (simpleText != null && !simpleText.isEmpty()) {
+ return simpleText;
+ }
+ StringBuilder text = new StringBuilder();
+ for (XWPFParagraph p : cell.getParagraphs()) {
+ String pt = getParagraphText(p);
+ if (pt != null) {
+ text.append(pt).append(" ");
+ }
+ }
+ return text.toString().trim();
+ }
+
+ @Override
+ public int getOrder() {
+ return 10;
+ }
+
+ private String getExtension(String fileName) {
+ if (fileName == null || !fileName.contains(".")) return null;
+ int lastDot = fileName.lastIndexOf('.');
+ return fileName.substring(lastDot + 1);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/HtmlExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/HtmlExtractor.java
new file mode 100644
index 0000000..51ebcbf
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/HtmlExtractor.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor.impl;
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.source.DocumentSource;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.select.Elements;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+
+/**
+ * 增强版 HTML 文档提取器
+ * 支持可配置的噪音过滤规则(含中文网站常见广告)
+ */
+public class HtmlExtractor implements FileExtractor {
+
+ private static final Set SUPPORTED_MIME_TYPES;
+ private static final Set SUPPORTED_EXTENSIONS;
+
+ static {
+ Set mimeTypes = new HashSet<>();
+ mimeTypes.add("text/html");
+ mimeTypes.add("application/xhtml+xml");
+ SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
+
+ Set extensions = new HashSet<>();
+ extensions.add("html");
+ extensions.add("htm");
+ extensions.add("xhtml");
+ extensions.add("mhtml");
+ SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
+ }
+
+ // 噪音过滤规则
+ private static final Set DEFAULT_SELECTORS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ "script", "style", "noscript",
+ "nav", "header", "footer", "aside",
+ "iframe", "embed", "object", "video", "audio",
+ ".ads", ".advertisement", ".ad-", "ad:",
+ ".sidebar", ".sider", ".widget", ".module",
+ ".breadcrumb", ".pager", ".pagination",
+ ".share", ".social", ".like", ".subscribe",
+ ".cookie", ".consent", ".banner", ".popup",
+ "[data-ad]", "[data-testid*='ad']", "[data-type='advertisement']"
+ )));
+
+ private static final Set CLASS_KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ "ad", "adv", "advertisement", "banner", "sponsor",
+ "sidebar", "sider", "widget", "module", "recommend",
+ "related", "similar", "youlike", "hot", "tuijian",
+ "share", "social", "like", "follow", "subscribe",
+ "cookie", "consent", "popup", "modal", "dialog",
+ "footer", "nav", "breadcrumb", "pager", "pagination"
+ )));
+
+ private static final Pattern ID_CLASS_PATTERN = Pattern.compile("(?i)\\b(" +
+ String.join("|",
+ "ad", "adv", "advertisement", "banner", "sponsor",
+ "sidebar", "sider", "widget", "module", "tuijian",
+ "share", "social", "like", "follow", "subscribe",
+ "cookie", "consent", "popup", "modal", "dialog",
+ "footer", "nav", "breadcrumb", "pager", "pagination"
+ ) + ")\\b"
+ );
+
+ // 可动态添加的自定义规则
+ private static final Set CUSTOM_SELECTORS = ConcurrentHashMap.newKeySet();
+ private static final Set CUSTOM_CLASS_KEYWORDS = ConcurrentHashMap.newKeySet();
+
+ /**
+ * 添加自定义噪音选择器(CSS 选择器)
+ */
+ public static void addCustomSelector(String selector) {
+ CUSTOM_SELECTORS.add(selector);
+ }
+
+ /**
+ * 添加自定义 class/id 关键词
+ */
+ public static void addCustomKeyword(String keyword) {
+ CUSTOM_CLASS_KEYWORDS.add(keyword.toLowerCase());
+ }
+
+ @Override
+ public boolean supports(DocumentSource source) {
+ String mimeType = source.getMimeType();
+ String fileName = source.getFileName();
+
+ if (mimeType != null && SUPPORTED_MIME_TYPES.contains(mimeType.toLowerCase())) {
+ return true;
+ }
+
+ if (fileName != null) {
+ String ext = getExtension(fileName);
+ if (ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String extractText(DocumentSource source) throws IOException {
+ try (InputStream is = source.openStream()) {
+ String html = readToString(is, StandardCharsets.UTF_8);
+ if (html.trim().isEmpty()) {
+ return "";
+ }
+
+ Document doc = Jsoup.parse(html);
+ doc.outputSettings().prettyPrint(false);
+
+ StringBuilder text = new StringBuilder();
+
+ extractTitle(doc, text);
+ extractBodyContent(doc, text);
+
+ return text.toString().trim();
+ } catch (Exception e) {
+ throw new IOException("Failed to parse HTML: " + e.getMessage(), e);
+ }
+ }
+
+ private void extractTitle(Document doc, StringBuilder text) {
+ Elements titleEl = doc.select("title");
+ if (!titleEl.isEmpty()) {
+ String title = titleEl.first().text().trim();
+ if (!title.isEmpty()) {
+ text.append(title).append("\n\n");
+ }
+ }
+ }
+
+ private void extractBodyContent(Document doc, StringBuilder text) {
+ Element body = doc.body();
+ if (body == null) return;
+
+ // 1. 移除已知噪音元素(CSS 选择器)
+ removeElementsBySelectors(body);
+
+ // 2. 移除 class/id 包含关键词的元素
+ removeElementsWithKeywords(body);
+
+ // 3. 遍历剩余节点
+ for (Node node : body.childNodes()) {
+ appendNodeText(node, text, 0);
+ }
+ }
+
+ /**
+ * 使用 CSS 选择器移除噪音
+ */
+ private void removeElementsBySelectors(Element body) {
+ List allSelectors = new ArrayList<>(DEFAULT_SELECTORS);
+ allSelectors.addAll(CUSTOM_SELECTORS);
+
+ for (String selector : allSelectors) {
+ try {
+ body.select(selector).remove();
+ } catch (Exception e) {
+ // 忽略无效选择器
+ }
+ }
+ }
+
+ /**
+ * 移除 class 或 id 包含关键词的元素
+ */
+ private void removeElementsWithKeywords(Element body) {
+ // 合并默认和自定义关键词
+ Set keywords = new HashSet<>(CLASS_KEYWORDS);
+ keywords.addAll(CUSTOM_CLASS_KEYWORDS);
+
+ // 使用 DFS 遍历所有元素
+ Deque stack = new ArrayDeque<>();
+ stack.push(body);
+
+ while (!stack.isEmpty()) {
+ Element el = stack.pop();
+
+ // 检查 class 或 id 是否匹配
+ String classes = el.className().toLowerCase();
+ String id = el.id().toLowerCase();
+
+ for (String keyword : keywords) {
+ if (classes.contains(keyword) || id.contains(keyword)) {
+ el.remove(); // 移除整个元素
+ break;
+ }
+ }
+
+ // 匹配正则模式
+ if (ID_CLASS_PATTERN.matcher(classes).find() ||
+ ID_CLASS_PATTERN.matcher(id).find()) {
+ el.remove();
+ continue;
+ }
+
+ // 将子元素加入栈
+ for (Element child : el.children()) {
+ stack.push(child);
+ }
+ }
+ }
+
+
+ private String repeat(String string, int times) {
+ if (times <= 0) return "";
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < times; i++) {
+ sb.append(string);
+ }
+ return sb.toString();
+ }
+
+ // 节点文本提取
+ private void appendNodeText(Node node, StringBuilder text, int level) {
+ if (node == null) return;
+
+ if (node instanceof org.jsoup.nodes.TextNode) {
+ String txt = ((org.jsoup.nodes.TextNode) node).text().trim();
+ if (!txt.isEmpty()) {
+ text.append(txt).append(" ");
+ }
+ } else if (node instanceof Element) {
+ Element el = (Element) node;
+ String tagName = el.tagName().toLowerCase();
+
+ if (tagName.matches("h[1-6]")) {
+ text.append("\n")
+ .append(repeat("##", Integer.parseInt(tagName.substring(1))))
+ .append(el.text().trim())
+ .append("\n\n");
+ } else if (tagName.equals("p")) {
+ String paraText = el.text().trim();
+ if (!paraText.isEmpty()) {
+ text.append(paraText).append("\n\n");
+ }
+ } else if (tagName.equals("li")) {
+ text.append("- ").append(el.text().trim()).append("\n");
+ } else if (tagName.equals("table")) {
+ extractTable(el, text);
+ text.append("\n");
+ } else if (tagName.equals("br")) {
+ text.append("\n");
+ } else if (tagName.equals("a")) {
+ String href = el.attr("href");
+ String textPart = el.text().trim();
+ if (!textPart.isEmpty()) {
+ text.append(textPart);
+ if (!href.isEmpty() && !href.equals(textPart)) {
+ text.append(" [").append(href).append("]");
+ }
+ text.append(" ");
+ }
+ } else if (isBlockLevel(tagName)) {
+ text.append("\n");
+ for (Node child : el.childNodes()) {
+ appendNodeText(child, text, level + 1);
+ }
+ text.append("\n");
+ } else {
+ for (Node child : el.childNodes()) {
+ appendNodeText(child, text, level);
+ }
+ }
+ }
+ }
+
+ private boolean isBlockLevel(String tagName) {
+ Set blockTags = new HashSet<>(Arrays.asList(
+ "div", "p", "h1", "h2", "h3", "h4", "h5", "h6",
+ "ul", "ol", "li", "table", "tr", "td", "th",
+ "blockquote", "pre", "section", "article", "figure"
+ ));
+ return blockTags.contains(tagName);
+ }
+
+ private void extractTable(Element table, StringBuilder text) {
+ text.append("[Table Start]\n");
+ Elements rows = table.select("tr");
+ for (Element row : rows) {
+ Elements cells = row.select("td, th");
+ List cellTexts = new ArrayList<>();
+ for (Element cell : cells) {
+ cellTexts.add(cell.text().trim());
+ }
+ text.append(String.join(" | ", cellTexts)).append("\n");
+ }
+ text.append("[Table End]\n");
+ }
+
+ private String readToString(InputStream is, java.nio.charset.Charset charset) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ try (java.io.InputStreamReader reader = new java.io.InputStreamReader(is, charset);
+ java.io.BufferedReader br = new java.io.BufferedReader(reader)) {
+ char[] buffer = new char[8192];
+ int read;
+ while ((read = br.read(buffer)) != -1) {
+ sb.append(buffer, 0, read);
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int getOrder() {
+ return 12;
+ }
+
+ private String getExtension(String fileName) {
+ if (fileName == null || !fileName.contains(".")) return null;
+ int lastDot = fileName.lastIndexOf('.');
+ return fileName.substring(lastDot + 1);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PdfTextExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PdfTextExtractor.java
new file mode 100644
index 0000000..8c28c08
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PdfTextExtractor.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor.impl;
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.source.DocumentSource;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * PDF 文本提取器
+ * 支持标准 PDF(非扫描件)
+ */
+public class PdfTextExtractor implements FileExtractor {
+
+ private static final Set SUPPORTED_MIME_TYPES;
+ private static final Set SUPPORTED_EXTENSIONS;
+
+ static {
+ Set mimeTypes = new HashSet<>();
+ mimeTypes.add("application/pdf");
+ SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
+
+ Set extensions = new HashSet<>();
+ extensions.add("pdf");
+ SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
+ }
+
+ @Override
+ public boolean supports(DocumentSource source) {
+ String mimeType = source.getMimeType();
+ String fileName = source.getFileName();
+
+ if (mimeType != null && SUPPORTED_MIME_TYPES.contains(mimeType)) {
+ return true;
+ }
+
+ if (fileName != null) {
+ String ext = getExtension(fileName);
+ return "pdf".equalsIgnoreCase(ext);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String extractText(DocumentSource source) throws IOException {
+ try (InputStream is = source.openStream();
+ PDDocument doc = PDDocument.load(is)) {
+ PDFTextStripper stripper = new PDFTextStripper();
+ return stripper.getText(doc).trim();
+ } catch (Exception e) {
+ throw new IOException("Failed to extract PDF text: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public int getOrder() {
+ return 10;
+ }
+
+ private String getExtension(String fileName) {
+ if (fileName == null || !fileName.contains(".")) return null;
+ int lastDot = fileName.lastIndexOf('.');
+ return fileName.substring(lastDot + 1);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PlainTextExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PlainTextExtractor.java
new file mode 100644
index 0000000..2029f8a
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PlainTextExtractor.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor.impl;
+
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.source.DocumentSource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 纯文本文件提取器(支持 UTF-8、GBK、GB2312 编码自动检测)
+ * 支持 .txt, .md, .log, .csv, .json, .xml 等文本格式
+ */
+public class PlainTextExtractor implements FileExtractor {
+
+ private static final Set SUPPORTED_MIME_TYPES;
+ private static final Set SUPPORTED_EXTENSIONS;
+
+ static {
+ Set mimeTypes = new HashSet<>();
+ mimeTypes.add("text/plain");
+ mimeTypes.add("text/markdown");
+ mimeTypes.add("text/csv");
+ mimeTypes.add("application/json");
+ mimeTypes.add("application/xml");
+ SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
+
+ Set extensions = new HashSet<>();
+ extensions.add("txt");
+ extensions.add("text");
+ extensions.add("md");
+ extensions.add("markdown");
+ extensions.add("log");
+ extensions.add("csv");
+ extensions.add("json");
+ extensions.add("xml");
+ extensions.add("yml");
+ extensions.add("yaml");
+ extensions.add("properties");
+ extensions.add("conf");
+ SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
+ }
+
+ @Override
+ public boolean supports(DocumentSource source) {
+ String mimeType = source.getMimeType();
+ String fileName = source.getFileName();
+
+ if (mimeType != null && (mimeType.startsWith("text/") || SUPPORTED_MIME_TYPES.contains(mimeType))) {
+ return true;
+ }
+
+ if (fileName != null) {
+ String ext = getExtension(fileName);
+ return ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase());
+ }
+
+ return false;
+ }
+
+ @Override
+ public String extractText(DocumentSource source) throws IOException {
+ try (InputStream is = source.openStream()) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, "utf-8"))) {
+ StringBuilder text = new StringBuilder();
+ char[] buffer = new char[8192];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ text.append(buffer, 0, read);
+ }
+ return text.toString().trim();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ @Override
+ public int getOrder() {
+ return 5; // 高优先级
+ }
+
+ private String getExtension(String fileName) {
+ if (fileName == null || !fileName.contains(".")) return null;
+ int lastDot = fileName.lastIndexOf('.');
+ return fileName.substring(lastDot + 1).toLowerCase();
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PptxExtractor.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PptxExtractor.java
new file mode 100644
index 0000000..bc2f085
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/extractor/impl/PptxExtractor.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.extractor.impl;
+
+import com.easyagents.core.file2text.extractor.FileExtractor;
+import com.easyagents.core.file2text.source.DocumentSource;
+import org.apache.poi.xslf.usermodel.*;
+import org.apache.xmlbeans.XmlException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * PPTX 文档提取器(.pptx)
+ * 提取幻灯片中的标题、段落、表格文本
+ */
+public class PptxExtractor implements FileExtractor {
+
+ private static final Set SUPPORTED_MIME_TYPES;
+ private static final String MIME_PREFIX = "application/vnd.openxmlformats-officedocument.presentationml";
+ private static final Set SUPPORTED_EXTENSIONS;
+
+ static {
+ // 精确 MIME(可选)
+ Set mimeTypes = new HashSet<>();
+ mimeTypes.add("application/vnd.openxmlformats-officedocument.presentationml.presentation");
+ mimeTypes.add("application/vnd.openxmlformats-officedocument.presentationml.slideshow");
+ SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
+
+ // 支持的扩展名
+ Set extensions = new HashSet<>();
+ extensions.add("pptx");
+ extensions.add("ppsx");
+ extensions.add("potx");
+ SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
+ }
+
+ @Override
+ public boolean supports(DocumentSource source) {
+ String mimeType = source.getMimeType();
+ String fileName = source.getFileName();
+
+ // 1. MIME 精确匹配
+ if (mimeType != null && SUPPORTED_MIME_TYPES.contains(mimeType)) {
+ return true;
+ }
+
+ // 2. MIME 前缀匹配
+ if (mimeType != null && mimeType.startsWith(MIME_PREFIX)) {
+ return true;
+ }
+
+ // 3. 扩展名匹配
+ if (fileName != null) {
+ String ext = getExtension(fileName);
+ if (ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String extractText(DocumentSource source) throws IOException {
+ StringBuilder text = new StringBuilder();
+
+ try (InputStream is = source.openStream();
+ XMLSlideShow slideShow = new XMLSlideShow(is)) {
+
+ List slides = slideShow.getSlides();
+
+ for (int i = 0; i < slides.size(); i++) {
+ XSLFSlide slide = slides.get(i);
+ text.append("\n--- Slide ").append(i + 1).append(" ---\n");
+
+ // 提取所有形状中的文本
+ for (XSLFShape shape : slide.getShapes()) {
+ if (shape instanceof XSLFTextShape) {
+ XSLFTextShape textShape = (XSLFTextShape) shape;
+ String shapeText = textShape.getText();
+ if (shapeText != null && !shapeText.trim().isEmpty()) {
+ text.append(shapeText).append("\n");
+ }
+ }
+ }
+
+ // 可选:提取表格
+ extractTablesFromSlide(slide, text);
+ }
+
+ } catch (XmlException e) {
+ throw new IOException("Invalid PPTX structure: " + e.getMessage(), e);
+ } catch (Exception e) {
+ throw new IOException("Failed to extract PPTX: " + e.getMessage(), e);
+ }
+
+ return text.toString().trim();
+ }
+
+ /**
+ * 提取幻灯片中的表格内容
+ */
+ private void extractTablesFromSlide(XSLFSlide slide, StringBuilder text) {
+ for (XSLFShape shape : slide.getShapes()) {
+ if (shape instanceof XSLFTable) {
+ XSLFTable table = (XSLFTable) shape;
+ text.append("\n[Table Start]\n");
+ for (XSLFTableRow row : table.getRows()) {
+ List cellTexts = new ArrayList<>();
+ for (XSLFTableCell cell : row.getCells()) {
+ String cellText = cell.getText();
+ cellTexts.add(cellText != null ? cellText.trim() : "");
+ }
+ text.append(String.join(" | ", cellTexts)).append("\n");
+ }
+ text.append("[Table End]\n");
+ }
+ }
+ }
+
+ @Override
+ public int getOrder() {
+ return 10;
+ }
+
+ private String getExtension(String fileName) {
+ if (fileName == null || !fileName.contains(".")) return null;
+ int lastDot = fileName.lastIndexOf('.');
+ return fileName.substring(lastDot + 1);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/ByteArrayDocumentSource.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/ByteArrayDocumentSource.java
new file mode 100644
index 0000000..0e7bd29
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/ByteArrayDocumentSource.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.source;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class ByteArrayDocumentSource implements DocumentSource {
+ private final byte[] data;
+ private final String fileName;
+ private final String mimeType;
+
+ public ByteArrayDocumentSource(byte[] data, String fileName) {
+ this(data, fileName, null);
+ }
+
+ public ByteArrayDocumentSource(byte[] data, String fileName, String mimeType) {
+ this.data = data.clone();
+ this.fileName = fileName;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public String getFileName() {
+ return fileName;
+ }
+
+ @Override
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ @Override
+ public InputStream openStream() {
+ return new ByteArrayInputStream(data);
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/ByteStreamDocumentSource.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/ByteStreamDocumentSource.java
new file mode 100644
index 0000000..db66962
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/ByteStreamDocumentSource.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.source;
+
+
+import com.easyagents.core.file2text.util.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class ByteStreamDocumentSource implements DocumentSource {
+ private final byte[] data;
+ private final String fileName;
+ private final String mimeType;
+
+ public ByteStreamDocumentSource(InputStream inputStream, String fileName) {
+ this(inputStream, fileName, null);
+ }
+
+ public ByteStreamDocumentSource(InputStream inputStream, String fileName, String mimeType) {
+ this.data = IOUtils.toByteArray(inputStream, Integer.MAX_VALUE);
+ this.fileName = fileName;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public String getFileName() {
+ return fileName;
+ }
+
+ @Override
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ @Override
+ public InputStream openStream() {
+ return new ByteArrayInputStream(data);
+ }
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/DocumentSource.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/DocumentSource.java
new file mode 100644
index 0000000..72bd613
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/DocumentSource.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.source;
+
+
+import java.io.InputStream;
+
+public interface DocumentSource {
+ String getFileName();
+
+ String getMimeType();
+
+ InputStream openStream() throws Exception;
+
+ default void cleanup() {
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/FileDocumentSource.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/FileDocumentSource.java
new file mode 100644
index 0000000..64b91bf
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/FileDocumentSource.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.source;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public class FileDocumentSource implements DocumentSource {
+ private final File file;
+ private final String mimeType;
+
+ public FileDocumentSource(File file) {
+ this(file, null);
+ }
+
+ public FileDocumentSource(File file, String mimeType) {
+ this.file = file;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public String getFileName() {
+ return file.getName();
+ }
+
+ @Override
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ @Override
+ public InputStream openStream() {
+ try {
+ return new FileInputStream(file);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/HttpDocumentSource.java b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/HttpDocumentSource.java
new file mode 100644
index 0000000..fa08d2e
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/file2text/source/HttpDocumentSource.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.file2text.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class IOUtils {
+ private static final int BUFFER_SIZE = 8192;
+
+ public static byte[] toByteArray(InputStream is, long maxSize) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ copyStream(is, buffer, maxSize);
+ return buffer.toByteArray();
+ }
+
+ public static void copyStream(InputStream is, OutputStream os, long maxSize) {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bytesRead;
+ long total = 0;
+ try {
+ while ((bytesRead = is.read(buffer)) != -1) {
+ if (total + bytesRead > maxSize) {
+ throw new RuntimeException("Stream too large: limit is " + maxSize + " bytes");
+ }
+ os.write(buffer, 0, bytesRead);
+ total += bytesRead;
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/memory/ChatMemory.java b/easy-agents-core/src/main/java/com/easyagents/core/memory/ChatMemory.java
new file mode 100644
index 0000000..5cdd715
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/memory/ChatMemory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.memory;
+
+import com.easyagents.core.message.Message;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface ChatMemory extends Memory {
+
+ List getMessages(int count);
+
+ void addMessage(Message message);
+
+ default void addMessages(Collection messages){
+ for (Message message : messages) {
+ addMessage(message);
+ }
+ }
+
+ void clear();
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/memory/DefaultChatMemory.java b/easy-agents-core/src/main/java/com/easyagents/core/memory/DefaultChatMemory.java
new file mode 100644
index 0000000..613422e
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/memory/DefaultChatMemory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.memory;
+
+import com.easyagents.core.message.Message;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class DefaultChatMemory implements ChatMemory {
+ private final Object id;
+ private final List messages = new ArrayList<>();
+
+ public DefaultChatMemory() {
+ this.id = UUID.randomUUID().toString();
+ }
+
+ public DefaultChatMemory(Object id) {
+ this.id = id;
+ }
+
+ @Override
+ public Object id() {
+ return id;
+ }
+
+ @Override
+ public List getMessages(int count) {
+ if (count <= 0) {
+ throw new IllegalArgumentException("count must be greater than 0");
+ }
+ if (count >= messages.size()) {
+ // 返回副本,避免修改原始消息
+ return new ArrayList<>(messages);
+ } else {
+ return messages.subList(messages.size() - count, messages.size());
+ }
+ }
+
+ @Override
+ public void addMessage(Message message) {
+ messages.add(message);
+ }
+
+ @Override
+ public void clear() {
+ messages.clear();
+ }
+
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/memory/Memory.java b/easy-agents-core/src/main/java/com/easyagents/core/memory/Memory.java
new file mode 100644
index 0000000..063574e
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/memory/Memory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.memory;
+
+import java.io.Serializable;
+
+public interface Memory extends Serializable {
+
+ Object id();
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/memory/package-info.java b/easy-agents-core/src/main/java/com/easyagents/core/memory/package-info.java
new file mode 100644
index 0000000..783e8e1
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/memory/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.memory;
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/message/AbstractTextMessage.java b/easy-agents-core/src/main/java/com/easyagents/core/message/AbstractTextMessage.java
new file mode 100644
index 0000000..4ac3d41
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/message/AbstractTextMessage.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
+
+import com.easyagents.core.util.Copyable;
+
+public abstract class AbstractTextMessage>
+ extends Message implements Copyable {
+
+ protected String content;
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public String getTextContent() {
+ return content;
+ }
+
+ /**
+ * 创建并返回当前对象的副本。
+ *
+ * @return 一个新的、内容相同但内存独立的对象
+ */
+ @Override
+ public abstract T copy();
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/message/AiMessage.java b/easy-agents-core/src/main/java/com/easyagents/core/message/AiMessage.java
new file mode 100644
index 0000000..7daa0b7
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/message/AiMessage.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
+
+import com.easyagents.core.util.Metadata;
+
+/**
+ * 表示一个通用的消息(Message),通常用于与大语言模型(如 OpenAI)进行交互。
+ * 消息内容可以是纯文本,也可以是多模态内容(例如:文本 + 图像等)。
+ *
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
+
+import java.util.HashMap;
+
+public class SystemMessage extends AbstractTextMessage {
+
+ public SystemMessage() {
+ }
+
+ public SystemMessage(String content) {
+ this.content = content;
+ }
+
+ public static SystemMessage of(String content) {
+ return new SystemMessage(content);
+ }
+
+ @Override
+ public String toString() {
+ return "SystemMessage{" +
+ "content='" + content + '\'' +
+ ", metadataMap=" + metadataMap +
+ '}';
+ }
+
+ /**
+ * 创建并返回当前对象的副本。
+ *
+ * @return 一个新的、内容相同但内存独立的对象
+ */
+ @Override
+ public SystemMessage copy() {
+ SystemMessage copy = new SystemMessage();
+ copy.content = this.content;
+ if (this.metadataMap != null) {
+ copy.metadataMap = new HashMap<>(this.metadataMap);
+ }
+ return copy;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/message/ToolCall.java b/easy-agents-core/src/main/java/com/easyagents/core/message/ToolCall.java
new file mode 100644
index 0000000..0c89ddf
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/message/ToolCall.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
+
+import com.easyagents.core.util.Copyable;
+import com.easyagents.core.util.Maps;
+import com.alibaba.fastjson2.JSON;
+
+import java.io.Serializable;
+import java.util.Map;
+
+public class ToolCall implements Serializable, Copyable {
+
+ private String id;
+ private String name;
+ private String arguments;
+
+ public ToolCall() {
+ }
+
+ public ToolCall(String id, String name, String arguments) {
+ this.id = id;
+ this.name = name;
+ this.arguments = arguments;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getArguments() {
+ return arguments;
+ }
+
+ public void setArguments(String arguments) {
+ this.arguments = arguments;
+ }
+
+ public Map getArgsMap() {
+ if (arguments == null || arguments.isEmpty()) {
+ return null;
+ }
+
+ String jsonStr = arguments.trim();
+
+ try {
+ return JSON.parseObject(jsonStr);
+ } catch (Exception e) {
+ if (jsonStr.contains("{") && jsonStr.contains("}")) {
+ String json = jsonStr.substring(jsonStr.indexOf("{"), jsonStr.lastIndexOf("}") + 1);
+ return JSON.parseObject(json);
+ }
+
+ if (!jsonStr.startsWith("{")) jsonStr = "{" + jsonStr;
+ if (!jsonStr.endsWith("}")) jsonStr = jsonStr + "}";
+
+ return JSON.parseObject(jsonStr);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ToolCall{" +
+ "id='" + id + '\'' +
+ ", name='" + name + '\'' +
+ ", arguments='" + arguments + '\'' +
+ '}';
+ }
+
+ public String toJsonString() {
+ return Maps.of("id", id)
+ .set("name", name)
+ .set("arguments", arguments)
+ .toJSON();
+ }
+
+ /**
+ * 创建并返回当前对象的副本。
+ *
+ * @return 一个新的、内容相同但内存独立的对象
+ */
+ @Override
+ public ToolCall copy() {
+ ToolCall copy = new ToolCall();
+ copy.id = this.id;
+ copy.name = this.name;
+ copy.arguments = this.arguments;
+ return copy;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/message/ToolMessage.java b/easy-agents-core/src/main/java/com/easyagents/core/message/ToolMessage.java
new file mode 100644
index 0000000..4f9e5e9
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/message/ToolMessage.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
+
+import java.util.HashMap;
+
+public class ToolMessage extends AbstractTextMessage {
+
+ private String toolCallId;
+
+ public String getToolCallId() {
+ return toolCallId;
+ }
+
+ public void setToolCallId(String toolCallId) {
+ this.toolCallId = toolCallId;
+ }
+
+
+ @Override
+ public String toString() {
+ return "ToolMessage{" +
+ "toolCallId='" + toolCallId + '\'' +
+ ", content='" + content + '\'' +
+ ", metadataMap=" + metadataMap +
+ '}';
+ }
+
+ /**
+ * 创建并返回当前对象的副本。
+ *
+ * @return 一个新的、内容相同但内存独立的对象
+ */
+ @Override
+ public ToolMessage copy() {
+ ToolMessage copy = new ToolMessage();
+ copy.content = this.content;
+ copy.toolCallId = this.toolCallId;
+ if (this.metadataMap != null) {
+ copy.metadataMap = new HashMap<>(this.metadataMap);
+ }
+ return copy;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/message/UserMessage.java b/easy-agents-core/src/main/java/com/easyagents/core/message/UserMessage.java
new file mode 100644
index 0000000..adc1a1e
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/message/UserMessage.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
+
+import com.easyagents.core.model.chat.ChatConfig;
+import com.easyagents.core.model.chat.tool.Tool;
+import com.easyagents.core.model.chat.tool.ToolScanner;
+import com.easyagents.core.util.ImageUtil;
+
+import java.io.File;
+import java.util.*;
+
+public class UserMessage extends AbstractTextMessage {
+
+ private List audioUrls;
+ private List videoUrls;
+ private List imageUrls;
+ private List tools;
+ private String toolChoice;
+
+ public UserMessage() {
+ }
+
+ public UserMessage(String content) {
+ setContent(content);
+ }
+
+ public void addTool(Tool tool) {
+ if (this.tools == null)
+ this.tools = new java.util.ArrayList<>();
+ this.tools.add(tool);
+ }
+
+ public void addTools(Collection extends Tool> functions) {
+ if (this.tools == null) {
+ this.tools = new java.util.ArrayList<>();
+ }
+ if (functions != null) {
+ this.tools.addAll(functions);
+ }
+ }
+
+ public void addToolsFromClass(Class> funcClass, String... methodNames) {
+ if (this.tools == null)
+ this.tools = new java.util.ArrayList<>();
+ this.tools.addAll(ToolScanner.scan(funcClass, methodNames));
+ }
+
+ public void addToolsFromObject(Object funcObject, String... methodNames) {
+ if (this.tools == null)
+ this.tools = new java.util.ArrayList<>();
+ this.tools.addAll(ToolScanner.scan(funcObject, methodNames));
+ }
+
+ public List getTools() {
+ return tools;
+ }
+
+ public Map getToolsMap() {
+ if (tools == null) {
+ return Collections.emptyMap();
+ }
+ Map map = new HashMap<>(tools.size());
+ for (Tool tool : tools) {
+ map.put(tool.getName(), tool);
+ }
+ return map;
+ }
+
+ public void setTools(List extends Tool> tools) {
+ if (tools == null) {
+ this.tools = null;
+ } else {
+ this.tools = new ArrayList<>(tools);
+ }
+ }
+
+ public String getToolChoice() {
+ return toolChoice;
+ }
+
+ public void setToolChoice(String toolChoice) {
+ this.toolChoice = toolChoice;
+ }
+
+
+ /// /// Audio
+ public List getAudioUrls() {
+ return audioUrls;
+ }
+
+ public void setAudioUrls(List audioUrls) {
+ this.audioUrls = audioUrls;
+ }
+
+ public void addAudioUrl(String audioUrl) {
+ if (audioUrls == null) {
+ audioUrls = new ArrayList<>(1);
+ }
+ audioUrls.add(audioUrl);
+ }
+
+
+ /// /// Video
+ public List getVideoUrls() {
+ return videoUrls;
+ }
+
+ public void setVideoUrls(List videoUrls) {
+ this.videoUrls = videoUrls;
+ }
+
+ public void addVideoUrl(String videoUrl) {
+ if (videoUrls == null) {
+ videoUrls = new ArrayList<>(1);
+ }
+ videoUrls.add(videoUrl);
+ }
+
+
+ /// /// Images
+ public List getImageUrls() {
+ return imageUrls;
+ }
+
+ public List getImageUrlsForChat(ChatConfig config) {
+ if (this.imageUrls == null) {
+ return null;
+ }
+ List result = new ArrayList<>(this.imageUrls.size());
+ for (String url : imageUrls) {
+ if (config != null && config.isSupportImageBase64Only()
+ && url.toLowerCase().startsWith("http")) {
+ url = ImageUtil.imageUrlToDataUri(url);
+ }
+ result.add(url);
+ }
+ return result;
+ }
+
+ public void setImageUrls(List imageUrls) {
+ this.imageUrls = imageUrls;
+ }
+
+ public void addImageUrl(String imageUrl) {
+ if (this.imageUrls == null) {
+ this.imageUrls = new ArrayList<>(1);
+ }
+ this.imageUrls.add(imageUrl);
+ }
+
+ public void addImageFile(File imageFile) {
+ addImageUrl(ImageUtil.imageFileToDataUri(imageFile));
+ }
+
+ public void addImageBytes(byte[] imageBytes, String mimeType) {
+ addImageUrl(ImageUtil.imageBytesToDataUri(imageBytes, mimeType));
+ }
+
+
+ @Override
+ public String toString() {
+ return "UserMessage{" +
+ "audioUrls=" + audioUrls +
+ ", videoUrls=" + videoUrls +
+ ", imageUrls=" + imageUrls +
+ ", functions=" + tools +
+ ", toolChoice='" + toolChoice + '\'' +
+ ", content='" + content + '\'' +
+ ", metadataMap=" + metadataMap +
+ '}';
+ }
+
+ /**
+ * 创建并返回当前对象的副本。
+ *
+ * @return 一个新的、内容相同但内存独立的对象
+ */
+ @Override
+ public UserMessage copy() {
+ UserMessage copy = new UserMessage();
+ copy.content = this.content;
+ copy.toolChoice = this.toolChoice;
+
+ // 深拷贝集合
+ if (this.audioUrls != null) copy.audioUrls = new ArrayList<>(this.audioUrls);
+ if (this.videoUrls != null) copy.videoUrls = new ArrayList<>(this.videoUrls);
+ if (this.imageUrls != null) copy.imageUrls = new ArrayList<>(this.imageUrls);
+ if (this.tools != null) copy.tools = new ArrayList<>(this.tools);
+
+ if (this.metadataMap != null) {
+ copy.metadataMap = new HashMap<>(this.metadataMap);
+ }
+
+ return copy;
+ }
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/message/package-info.java b/easy-agents-core/src/main/java/com/easyagents/core/message/package-info.java
new file mode 100644
index 0000000..68cc758
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/message/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.message;
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/model/chat/BaseChatModel.java b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/BaseChatModel.java
new file mode 100644
index 0000000..b4fbf8d
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/BaseChatModel.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.model.chat;
+
+import com.easyagents.core.model.chat.log.ChatMessageLogger;
+import com.easyagents.core.model.chat.response.AiMessageResponse;
+import com.easyagents.core.model.client.ChatClient;
+import com.easyagents.core.model.client.ChatRequestSpec;
+import com.easyagents.core.model.client.ChatRequestSpecBuilder;
+import com.easyagents.core.prompt.Prompt;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 支持责任链、统一上下文和协议客户端的聊天模型基类。
+ *
+ * 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
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.model.chat;
+
+import com.easyagents.core.model.client.ChatRequestSpec;
+import com.easyagents.core.prompt.Prompt;
+
+import java.util.Map;
+
+public class ChatContext {
+
+ Prompt prompt;
+ ChatConfig config;
+ ChatOptions options;
+ ChatRequestSpec requestSpec;
+ Map attributes;
+
+ public Prompt getPrompt() {
+ return prompt;
+ }
+
+ public void setPrompt(Prompt prompt) {
+ this.prompt = prompt;
+ }
+
+ public ChatConfig getConfig() {
+ return config;
+ }
+
+ public void setConfig(ChatConfig config) {
+ this.config = config;
+ }
+
+ public ChatOptions getOptions() {
+ return options;
+ }
+
+ public void setOptions(ChatOptions options) {
+ this.options = options;
+ }
+
+
+ public ChatRequestSpec getRequestSpec() {
+ return requestSpec;
+ }
+
+ public void setRequestSpec(ChatRequestSpec requestSpec) {
+ this.requestSpec = requestSpec;
+ }
+
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ public void addAttribute(String key, Object value) {
+ if (attributes == null) {
+ attributes = new java.util.HashMap<>();
+ }
+ attributes.put(key, value);
+ }
+
+ public void setAttributes(Map attributes) {
+ this.attributes = attributes;
+ }
+
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/model/chat/ChatContextHolder.java b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/ChatContextHolder.java
new file mode 100644
index 0000000..adcd32f
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/ChatContextHolder.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.model.chat;
+
+import com.easyagents.core.model.client.ChatRequestSpec;
+import com.easyagents.core.prompt.Prompt;
+
+/**
+ * 聊天上下文管理器,用于在当前线程中保存聊天相关的上下文信息。
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.model.chat;
+
+import com.easyagents.core.model.chat.response.AiMessageResponse;
+
+/**
+ * 聊天模型请求拦截器。
+ *
+ * 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
+ *
+ * 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 com.easyagents.core.model.chat;
+
+import com.easyagents.core.message.AiMessage;
+import com.easyagents.core.model.exception.ModelException;
+import com.easyagents.core.model.chat.response.AbstractBaseMessageResponse;
+import com.easyagents.core.model.chat.response.AiMessageResponse;
+import com.easyagents.core.prompt.Prompt;
+import com.easyagents.core.prompt.SimplePrompt;
+
+public interface ChatModel {
+
+ default String chat(String prompt) {
+ return chat(prompt, new ChatOptions());
+ }
+
+ default String chat(String prompt, ChatOptions options) {
+ AbstractBaseMessageResponse response = chat(new SimplePrompt(prompt), options);
+ if (response != null && response.isError()) {
+ throw new ModelException(response.getErrorMessage());
+ }
+ return response != null && response.getMessage() != null ? response.getMessage().getContent() : null;
+ }
+
+ default AiMessageResponse chat(Prompt prompt) {
+ return chat(prompt, new ChatOptions());
+ }
+
+ AiMessageResponse chat(Prompt prompt, ChatOptions options);
+
+ default void chatStream(String prompt, StreamResponseListener listener) {
+ this.chatStream(new SimplePrompt(prompt), listener, new ChatOptions());
+ }
+
+ default void chatStream(String prompt, StreamResponseListener listener, ChatOptions options) {
+ this.chatStream(new SimplePrompt(prompt), listener, options);
+ }
+
+ //chatStream
+ default void chatStream(Prompt prompt, StreamResponseListener listener) {
+ this.chatStream(prompt, listener, new ChatOptions());
+ }
+
+ void chatStream(Prompt prompt, StreamResponseListener listener, ChatOptions options);
+
+}
diff --git a/easy-agents-core/src/main/java/com/easyagents/core/model/chat/ChatObservabilityInterceptor.java b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/ChatObservabilityInterceptor.java
new file mode 100644
index 0000000..d7274fa
--- /dev/null
+++ b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/ChatObservabilityInterceptor.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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
+ *