初始化
This commit is contained in:
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
root = true
|
||||
|
||||
# 匹配全部文件
|
||||
[*]
|
||||
# 结尾换行符,可选"lf"、"cr"、"crlf"
|
||||
end_of_line = lf
|
||||
# 在文件结尾插入新行
|
||||
insert_final_newline = true
|
||||
# 删除一行中的前后空格
|
||||
trim_trailing_whitespace = true
|
||||
# 设置字符集
|
||||
charset = utf-8
|
||||
# 缩进风格,可选"space"、"tab"
|
||||
indent_style = space
|
||||
# 缩进的空格数
|
||||
indent_size = 4
|
||||
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
**/target/
|
||||
**/node_modules/
|
||||
.idea
|
||||
*.iml
|
||||
.project
|
||||
.settings
|
||||
starter/**/jboot.properties
|
||||
*.jar
|
||||
*.log
|
||||
.logs
|
||||
logs
|
||||
*.iws
|
||||
*.classpath
|
||||
.DS_Store
|
||||
*.patch
|
||||
~$*
|
||||
*/*.jar
|
||||
docker_volumes/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
/docs/.vitepress/cache/
|
||||
/docs/.vitepress/dist/
|
||||
/docs/pnpm-lock.yaml
|
||||
application-dev.yml
|
||||
.flattened-pom.xml
|
||||
87
README.md
Normal file
87
README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Easy-Agents
|
||||
|
||||
Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖从模型调用到工具执行、MCP 集成、检索与工作流编排的完整链路。
|
||||
|
||||
## 主要能力
|
||||
|
||||
- 多模型统一接入(Chat / Embedding / Rerank / Image)
|
||||
- Prompt 与消息上下文管理
|
||||
- Tool 定义、执行与拦截器机制
|
||||
- MCP 客户端能力(调用、拦截、缓存与管理)
|
||||
- 文档读取与切分、向量存储与检索
|
||||
- 工作流执行引擎(Flow)与 Easy-Agents 适配支持
|
||||
|
||||
## 模块说明
|
||||
|
||||
- `easy-agents-bom`:依赖版本管理(BOM)。
|
||||
- `easy-agents-core`:核心抽象与基础能力。
|
||||
- `easy-agents-chat`:对话模型接入实现集合。
|
||||
- `easy-agents-embedding`:向量化模型实现集合。
|
||||
- `easy-agents-rerank`:重排模型实现集合。
|
||||
- `easy-agents-image`:图像模型能力。
|
||||
- `easy-agents-store`:向量存储实现。
|
||||
- `easy-agents-search-engine`:检索引擎实现。
|
||||
- `easy-agents-tool`:工具调用能力。
|
||||
- `easy-agents-mcp`:MCP 集成。
|
||||
- `easy-agents-flow`:流程编排核心引擎。
|
||||
- `easy-agents-support`:Flow 与 Easy-Agents 适配模块。
|
||||
- `easy-agents-spring-boot-starter`:Spring Boot 自动配置支持。
|
||||
- `easy-agents-samples`:示例工程。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- JDK 8+
|
||||
- Maven 3.8+
|
||||
|
||||
## 构建与安装
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
mvn -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true clean install
|
||||
```
|
||||
|
||||
构建完成后,相关构件会安装到本地 Maven 仓库,可供 `easyflow` 等项目直接依赖。
|
||||
|
||||
## 快速示例
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
OpenAIChatModel chatModel = OpenAIChatConfig.builder()
|
||||
.provider("GiteeAI")
|
||||
.endpoint("https://ai.gitee.com")
|
||||
.requestPath("/v1/chat/completions")
|
||||
.apiKey("your-api-key")
|
||||
.model("Qwen3-32B")
|
||||
.buildModel();
|
||||
|
||||
String output = chatModel.chat("如何才能更幽默?");
|
||||
System.out.println(output);
|
||||
}
|
||||
```
|
||||
|
||||
## 在业务项目中引入(示例)
|
||||
|
||||
```xml
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-bom</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-flow</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-support</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
856
changes.md
Normal file
856
changes.md
Normal file
@@ -0,0 +1,856 @@
|
||||
# Easy-Agents ChangeLog
|
||||
|
||||
## v2.0.0-rc.6 20260127
|
||||
- fix(core): update message retrieval and context handling in streaming components
|
||||
- feat(http): add proxy authentication support to OkHttpClientUtil
|
||||
- feat(github): add issue templates for bug reports and documentation feedback
|
||||
|
||||
|
||||
## v2.0.0-rc.5 20260127
|
||||
- feat(core): add JSON error message detection and enhance embedding model error handling
|
||||
- feat(test): add ChatOptions support and enhance stream testing utilities
|
||||
- refactor(chat): rename extra field to extraBody in ChatOptions
|
||||
- fix(stream): handle failure flag properly in stream client listener
|
||||
- refactor(core): remove unused string utility and simplify thinking protocol logic
|
||||
- refactor(chat): rename variable for clarity in OpenAI request builder
|
||||
- refactor(chat): rename extra field to extraBody in ChatOptions
|
||||
- refactor(chat): remove default thinking protocol handling and add dynamic protocol detection
|
||||
- feat(chat): add thinking protocol support for chat configurations
|
||||
|
||||
|
||||
## v2.0.0-rc.4 20260123
|
||||
- fix: Optimizing Alibaba Cloud vector database vector failure prompt
|
||||
- feat(embedding): add dimensions and user support to embedding options
|
||||
- feat(document): add MarkdownHeaderSplitter for hierarchical document splitting
|
||||
- feat(chat): add reasoning content support for tool messages in chat configuration
|
||||
|
||||
|
||||
## v2.0.0-rc.3 20260112
|
||||
- feat(readme): add MCP capabilities to the list of features
|
||||
- fix(openai): remove unsupported top_k parameter from openai request
|
||||
- fix(openai): handle JSON parsing errors in OpenAI chat client
|
||||
- refactor(ollama): update configuration defaults and parameter naming
|
||||
- chore(ollama): clean up request spec builder code
|
||||
- refactor(qwen): remove response format option from QwenChatOptions
|
||||
- feat(chat): add response format support to chat options
|
||||
- chore(build): remove mvn17.sh script
|
||||
- feat(mcp): add methods to access client descriptor and health checker
|
||||
|
||||
|
||||
## v2.0.0-rc.2 20260108
|
||||
- fix(deps): update POI dependencies and remove slf4j-simple
|
||||
- fix(http): add null check for request headers in HttpClient
|
||||
|
||||
|
||||
## v2.0.0-rc.1 20260106
|
||||
- fix(OpenAIChatMessageSerializer): Added null check for parameters array to prevent NPE
|
||||
|
||||
|
||||
## v2.0.0-beta.9 20260105
|
||||
- refactor(stream): remove content check in stream client listener
|
||||
- fix(mcp): handle duplicate client registration properly prevent resource leaks when client is re-registered
|
||||
- refactor(message): rename shouldNewCall method to isNewCall for clarity
|
||||
- refactor(message): optimize tool call comparison logic in AiMessage
|
||||
- test(openai): update OpenAI chat model test with tool call support
|
||||
|
||||
|
||||
## v2.0.0-beta.8 20260104
|
||||
- fix(observability): correct OpenTelemetry propagator detection logic
|
||||
- fix(observability): improve OpenTelemetry SDK registration check
|
||||
- feat(observability): add OpenTelemetry SDK reuse check
|
||||
|
||||
|
||||
## v2.0.0-beta.7 20260104
|
||||
- refactor(docs): update MCP client method name in documentation
|
||||
- docs(chat): update MCP documentation with JDK version requirements
|
||||
- chore(build): set maven compiler properties to Java 8
|
||||
- docs(chat): add comprehensive MCP client integration guide
|
||||
- refactor(client): rename getClient to getMcpClient for clarity
|
||||
- refactor(mcp): move client-related classes to dedicated package
|
||||
- test(mcp): add comprehensive unit tests for McpClientManager
|
||||
- feat(config): add MCP server configuration file and Added mcp-servers.json with everything server configuration for test
|
||||
- refactor(mcp): replace McpRootConfig with McpConfig for server specifications
|
||||
- feat(mcp): add exception handling for MCP tool calls
|
||||
- fix(mcp): handle null and single text content in tool invocation
|
||||
- feat(mcp): add MCP tool retrieval functionality
|
||||
- feat(mcp): add McpTool implementation for MCP tool integration
|
||||
- feat(tool): add defaultValue property to Parameter class
|
||||
- chore(license): Updated copyright year from 2025 to 2026
|
||||
- (mcp):feat add Model Context Protocol client management module
|
||||
|
||||
|
||||
|
||||
## v2.0.0-beta.6 20251230
|
||||
- refactor(core): Replace clearFullMessage with finalizeFullMessage to properly handle content
|
||||
- fix(stream): clear full message content on stream completion
|
||||
- test(openai): update OpenAI chat model test configuration and weather function
|
||||
- refactor(OpenAIChatMessageSerializer): update tool message handling and serialization
|
||||
- feat(chat): add tool message support with compatibility for models that don't support native format
|
||||
- refactor(chat): update ChatConfig tool support methods and remove unused field
|
||||
- test(utils): add ChatModelTestUtils for stream testing
|
||||
- refactor(stream): simplify message handling and stop logic in BaseStreamClientListener
|
||||
- refactor(message): update AiMessage delta completion logic
|
||||
|
||||
|
||||
|
||||
## v2.0.0-beta.5 20251219
|
||||
- test(openai): remove unnecessary thread sleep in chat model tests
|
||||
- feat(chat): add includeUsage option for streaming responses
|
||||
- feat(openai): add stream chat test for bailian model
|
||||
- feat(core): add stopped flag and improve stream listener logic
|
||||
- fix(stream): handle final delta correctly in stream client listener and Wrapped final message notification in try-finally block
|
||||
- feat(openai): add support for extra options in chat request
|
||||
- docs(models): update embedding model translation
|
||||
|
||||
|
||||
|
||||
## v2.0.0-beta.4 20251210
|
||||
- fix(core):优化AI消息增量更新的空值判断
|
||||
- fix(core):修复工具调用合并逻辑
|
||||
- chore(deps): 移除嵌入模型相关依赖配置
|
||||
- fix(message):优化 ToolCall 参数解析逻辑
|
||||
- feat(core):优化聊天模型和客户端实现
|
||||
- fix: embedding module packaging
|
||||
- feat(bom): 添加嵌入模型依赖项
|
||||
|
||||
|
||||
## v2.0.0-beta.2 20251202
|
||||
- 修复:修复 Maven 依赖异常,更新版本号,感谢 @jieven
|
||||
|
||||
## v2.0.0-beta.1 20251126
|
||||
- 移除 Chain 模块,合并到 Tinyflow
|
||||
- 移除 documentParser 模块
|
||||
- 移除 DocumentLoader 模块
|
||||
- 新增 file2text 模块
|
||||
- 新增 observability 模块
|
||||
- 新增 RAG 执行 AI 对文档进行拆分
|
||||
- 修改 LLM 为 ChatModel
|
||||
- 修改 LLMClient 为 StreamClient
|
||||
- 修改 LLMClientListener 为 StreamClientListener
|
||||
- 修改 LLMConfig 为 ChatConfig
|
||||
- 修改 Function 为 Tool
|
||||
- 重命名 PromptFormat 为 MessageFormat
|
||||
- 重命名 TextPromptTemplate 为 PromptTemplate
|
||||
- 重命名 TextPrompt 为 SimplePrompt
|
||||
- 重命名 HistoriesPrompt 为 MemoryPrompt
|
||||
- 重命名 HumanMessage 等合并为 UserMessage
|
||||
|
||||
|
||||
## v1.4.2 20251117
|
||||
- 新增:新增本地 Token 计算的相关内容,方便在某些情况下无法获取外部 token 时使用
|
||||
- 新增: 新增 file2text 模块
|
||||
- 新增: Chain 支持主动去 暂停执行 或 恢复执行
|
||||
- 新增:feat: 新增可观测的相关能力
|
||||
- 优化:重构移动 ReAct 的包到 agents 目录下
|
||||
- 优化:优化 Chain.addEdge 自动为没有 id 的 edge 添加 id
|
||||
- 优化:Chain.addNode 自动为 node 添加 inwardEdges 和 outwardEdges
|
||||
- 优化:优化 ChainHolder 的属性类型定义,方便准确序列化
|
||||
- 优化:ChainEdge 移除无用的定义
|
||||
|
||||
|
||||
|
||||
## v1.4.1 20251023
|
||||
- 新增:prompt 格式化新增 `{{ aa ?? bb ?? 'default'}}` 取值语法
|
||||
- 优化:优化 chain.get 方法获取数据的性能
|
||||
- 优化:移除 gitee 旧的 api 支持(官方已下架)
|
||||
|
||||
|
||||
## v1.4.0 20251020
|
||||
- 修复: computeCost 可能等于 null 而出现 NPE 的问题
|
||||
|
||||
|
||||
## v1.3.9 20251019
|
||||
- 新增: 添加 Chain 关于算力消耗配置的功能
|
||||
- 新增: Chain 添加 loopNodeExecutionLimit 配置,保护由于用户配置不当可能造成死循环执行的问题
|
||||
- 新增: 优化监听器,当多个监听器发生时,有监听器发生错误不影响其他监听器执行
|
||||
- 新增: Node 支持配置积分消耗的计算逻辑和表达式
|
||||
- 新增: Chain 的取值支持三目运算
|
||||
- 修复:避免截断后添加 systemMessage 时导致 memory 中的 systemMessage 越来越多
|
||||
|
||||
|
||||
## v1.3.8 20251010
|
||||
- 新增:图片消息添加多张图片的支持
|
||||
- 新增:大模型节点添加图片识别的能力
|
||||
- 新增:大模型节点支持多种 json 格式输出
|
||||
- 优化:优化 okHttpclient 的请求机制,更轻量化
|
||||
- 优化:优化 OkHttpClientUtil 的配置支持环境变量
|
||||
- 优化:ImageUtil 支持自动添加 base64 url 前缀
|
||||
|
||||
|
||||
## v1.3.7 20250929
|
||||
- 新增:支持 Chroma 向量数据库(1.1.10),目前只支持单租户,单数据库(有默认且可配置),感谢 @joe.wen
|
||||
- 新增:TextPromptTemplate.formatToString 添加 escapeJson 参数配置的支持
|
||||
|
||||
|
||||
## v1.3.6 20250924
|
||||
- 修复:ConfirmNode 必须勾选必填才能正确进行数据确认的问题
|
||||
|
||||
|
||||
## v1.3.5 20250917
|
||||
- 新增: 新增 ChainNodeValidator 用于给 Node 自定义其验证器
|
||||
- 新增: 新增 ChainNode.validate 方法,用于校验其数据是否完善
|
||||
- 新增: 工作流执行新增节点算力消耗的定义
|
||||
- 新增: getParameterValues 通过 LinkedHashMap 返回属性,支持参数顺序
|
||||
- 优化:优化 HumanMessage 类的函数处理逻辑
|
||||
- 优化: 重构 Chain 的验证逻辑
|
||||
- 修复: 修复 Parameter 为非必填,但是也必须输入参数的问题
|
||||
- 修复: 在 js 执行节点可能出现 java.lang.NoClassDefFoundError: com/ibm/icu/number/Notation 错误的问题
|
||||
- 修复:正则表达式 `Action Input: (\{.<em>?})` 在匹配时会漏掉最后一个 `"}"` 的问题,感谢 @狂野流星
|
||||
|
||||
|
||||
## v1.3.4 20250817
|
||||
- 新增:Parameter 新增 formPlaceholder 属性
|
||||
|
||||
|
||||
## v1.3.3 20250806
|
||||
- 优化:优化合并 ConfirmParameter 到 Parameter,以支持更多的场景
|
||||
|
||||
|
||||
## v1.3.2 20250731
|
||||
- 优化:优化 AiMessageResponse.getFunctionCallers 方法
|
||||
- 修复:多轮 tool call 时,获取最后一条 HumanMessage 错误的问题
|
||||
- 修复:Chain 的 Parameter 类型为 Array 时,内容固定值无法解析的问题
|
||||
|
||||
|
||||
## v1.3.1 20250722
|
||||
- 新增:ChatOptions 新增 extra 配置,用于自定义大模型的参数内容
|
||||
- 修复:节点异步执行的情况下,可能出现 check 不正确的问题
|
||||
- 修复:jsExecNode 无法转换结果为 JsonObject 的问题
|
||||
- 优化:优化 getNodeContext 方法,只需要传入 id 值
|
||||
- 优化:移除 Moonshot ,使用 openai 替代
|
||||
|
||||
|
||||
## v1.2.9 20250718
|
||||
- 优化:优化 parent 和 children 的过度设计,使之逻辑更加简洁
|
||||
- 修复:修复 Node 包含子的 chain 时,会导致 json 解析错误的问题
|
||||
|
||||
|
||||
## v1.2.8 20250715
|
||||
- 新增: ReActAgent 添加 ChatOptions 的设置能力
|
||||
- 新增: ConfirmParameter 添加更多的配置参数支持
|
||||
- 优化: 优化 getParameterValues 的错误信息
|
||||
- 优化: 移除 chain 非必要的 error 日志
|
||||
- 优化: 优化 Chain.getParameterValues
|
||||
|
||||
|
||||
## v1.2.7 20250713
|
||||
- 新增:代码执行节点添加 “_context” 对象
|
||||
- 新增:新增 Chain.toJSON() 和 Chain.fromJSON() 方法
|
||||
- 新增:新增 ConfirmNode 用于支持用户手动确认的场景
|
||||
- 优化:优化 Chain.getParameterValues 方法
|
||||
- 优化:移动 Chain 监听器的相关类到 listener 包里去
|
||||
|
||||
|
||||
## v1.2.6 20250709
|
||||
- 新增: Chain 新增 getNodeExecuteResult 方法
|
||||
- 优化:优化重构 EdgeCondition 和 NodeCondition 方法
|
||||
|
||||
|
||||
## v1.2.5 20250707
|
||||
- 新增:新增 Audio 多模态的支持
|
||||
- 新增:新增 ReActMessageBuilder,允许用户构建自定义的消息
|
||||
- 优化:feat: 修改 TextAndImageMessage 和 TextAndAudioMessage 为 HumanImageMessage 和 HumanAudioMessage
|
||||
- 修复:DeepseekLlm 无法自动注入,配置 factories 文件
|
||||
|
||||
|
||||
## v1.2.4 20250701
|
||||
- 新增:在节点执行出错时,添加必要的错误日志输出
|
||||
- 新增:大模型 Parameter 添加子 Parameter 的配置支持
|
||||
- 新增:ReActAgent 添加 continueOnActionJsonParseError 和 continueOnActionInvokeError 配置
|
||||
- 修复:修复 EmbeddingModel.dimensions() 错误信息不友好的问题
|
||||
- 修复:tool call第二次请求模型时缺少了tools信息 close #ICG584
|
||||
|
||||
|
||||
## v1.2.3 20250626
|
||||
- 新增:节点添加循环执行的配置能力
|
||||
- 新增:新增 starter 中 deepseek 的配置支持,openai 可以指定 chatpath 属性
|
||||
- 新增:OpenAILlm 支持自定义 HttpClient
|
||||
- 优化:优化 bom 模块依赖冲突 close #ICG2TD
|
||||
- 优化:优化 ReActAgent 代码,新增更多的监听支持
|
||||
- 优化:优化 OkHttpClientUtil 的默认参数
|
||||
- 优化:重命名 JavascriptStringCondition 为 JsCodeCondition
|
||||
- 修复:修复 Gitee 生成图片错误的问题
|
||||
- 文档:更新 chain 的相关文档
|
||||
- 文档:更新优化 LLM 示例代码
|
||||
- 文档:优化 prompt 示例代码
|
||||
- 文档:节点循环示例代码
|
||||
|
||||
|
||||
## v1.2.2 20250618
|
||||
- 新增:新增 es 和 lucene 搜索引擎
|
||||
|
||||
|
||||
## v1.2.0 20250614
|
||||
- 新增:新增 "default" rerank 模型,用于对接多个不同的 rerank 服务
|
||||
- 新增:新增 ReAct Agent
|
||||
- 优化:挂起-恢复执行逻辑优化,每次执行节点移除挂起节点列表中该节点
|
||||
|
||||
|
||||
## v1.1.8 20250605
|
||||
- 新增:阿里云增加余弦相似度得分回显
|
||||
- 修复: 修正 Milvus 下 COSINE 相似度计算方法
|
||||
|
||||
|
||||
|
||||
## v1.1.7 20250604
|
||||
- 修复:使用 qwen-plus 调用 function_call 没有正确拼接大模型返回的参数问题
|
||||
- 修复: 修复 DeepseekLlmUtil 类型转换错误
|
||||
- 修复: HistoriesPrompt 的 toMessages 可能多次添加 systemMessage 的问题
|
||||
|
||||
|
||||
|
||||
## v1.1.5 20250603
|
||||
- 修复:修复 CodeNode 的 js 无法通过 "." 调用 map 数据的问题
|
||||
|
||||
|
||||
|
||||
## v1.1.4 20250530
|
||||
- 新增: 为 ChainStartEvent 和 ChainResumeEvent 添加获取初始化参数的功能
|
||||
- 优化: 优化 JsExecNode 在每次执行脚本时新建一个独立 Context
|
||||
- 优化: 优化 Event 的 toString
|
||||
- 修复: node 的状态在执行时未自动变化的问题
|
||||
|
||||
|
||||
|
||||
## v1.1.3 20250527
|
||||
- 修复:修复阿里云百炼 text-embedding-v3 向量化使用 milvus 使用默认向量模型导致两次维度不一致问题
|
||||
- 修复:qwen3 非流式返回设置 enable_thinking 为 false
|
||||
|
||||
|
||||
|
||||
## v1.1.2 20250524
|
||||
- 新增: StreamResponseListener 添加 onMatchedFunction 方法
|
||||
- 新增: 添加 openai 兼容 api 的其他第三方 api 测试
|
||||
- 优化: 添加 FunctionPrompt 的 toString 方法
|
||||
- 优化: 优化 ImagePrompt 的方法
|
||||
- 优化: 优化 ToolPrompt 支持多个方法调用
|
||||
- 优化: 优化 Stream 模型下的 Function Call
|
||||
- 优化: 优化 SseClient 的 tryToStop 方法
|
||||
- 优化: 优化 FunctionCall 以及添加 toString 方法
|
||||
- 优化: 优化 OpenAILlm.java
|
||||
|
||||
|
||||
|
||||
## v1.1.1 20250522
|
||||
- 新增:新增 NodeErrorListener 用于监听 node 的错误情况
|
||||
- 优化:重构 ChainErrorListener 的参数顺序
|
||||
- 优化:优化 getParameterValues 的默认值获取
|
||||
|
||||
|
||||
|
||||
## v1.1.0 20250516
|
||||
- 优化:增强 LLM 的 markdown 包裹优化
|
||||
- 优化:重命名 StringUtil.obtainFirstHasText 方法名称为 getFirstWithText
|
||||
- 修复:修复大模型节点,返回 json 内容时不正确的问题
|
||||
- 修复:修复 EndNode 在输出固定值时出现 NPE 的问题
|
||||
|
||||
|
||||
|
||||
## v1.0.9 20250513
|
||||
- 新增: Chain 添加 reset 方法,使之调用后可以执行多次
|
||||
- 优化:不允许设置默认 EmbeddingOptions 配置的 encodingFormat
|
||||
- 优化:修改模型思考过程的设置,让 content 和 reasoningContent 输出内容一致,感谢 @Alex
|
||||
|
||||
|
||||
|
||||
## v1.0.8 20250511
|
||||
- 优化:优化 elasticSearch 用户自定义集合名称就用用户自定义集合,没有传就用默认集合名称
|
||||
- 优化:从命名 TextPromptTemplate.create 方法名称为 TextPromptTemplate.of,更加符合 “缓存” 的特征
|
||||
- 修复:修复 openSearch 存储报错问题
|
||||
- 文档:添加提示词相关文档
|
||||
- 文档:添加 “模板缓存” 的相关文档
|
||||
- 测试:添加 milvus 向量存储用法示例测试类,感谢 @lyg
|
||||
|
||||
|
||||
|
||||
## v1.0.7 20250508
|
||||
- 新增: 添加 Milvus 的相识度返回
|
||||
- 新增: Chain.getParameterValues 添加对固定数据格式填充的能力
|
||||
- 优化: Parameter 添加 dataType 默认数据
|
||||
- 优化: TextPromptTemplate.create 添加缓存以提高性能
|
||||
|
||||
|
||||
|
||||
## v1.0.6 20250507
|
||||
- 新增: 增加 qdrant 向量数据库支持
|
||||
- 优化: 重构 TextPromptTemplate,使其支持更多的语法
|
||||
- 优化: 优化 pom 管理
|
||||
|
||||
|
||||
|
||||
## v1.0.5 20250430
|
||||
- 新增: 允许通过 ChatOptions 在运行时动态替换模型名称
|
||||
- 新增:增加是否开启思考模式参数,适用于 Qwen3 模型
|
||||
- 新增:Document 增加文档标题
|
||||
- 新增:增强知识库查询条件
|
||||
- 新增:优化 Chain 的 get 逻辑,支持获取对象的属性内容
|
||||
- 测试:添加通过 OpenAI 的 API 调用 Gitee 进行图片识别
|
||||
- 测试:添加 chain 的数据获取测试
|
||||
|
||||
|
||||
|
||||
## v1.0.4 20250427
|
||||
- 新增: 为 VectorData 添加 score 属性,统一文档 score 字段
|
||||
- 优化:重构 Chain 的异步执行逻辑
|
||||
|
||||
|
||||
|
||||
## v1.0.3 20250425
|
||||
- 新增: deepseek-r1 推理过程增量输出改为完整输出和内容的输出保持一致,感谢 @liutf
|
||||
- 新增: 增加 QwenChatOptions,让通义千问支持更多的参数,感谢 @liutf
|
||||
- 新增:新增 ChainHolder,用于序列化 ChainNode,以支持分布式执行
|
||||
- 优化:优化 Chain,在暂停时抛出异常
|
||||
|
||||
|
||||
|
||||
## v1.0.2 20250412
|
||||
- feat: add JavascriptStringCondition
|
||||
- refactor: move "description" property to ChainNode
|
||||
- test: add ChainConditionStringTest
|
||||
|
||||
---
|
||||
- 新增:添加 JavascriptStringCondition 条件
|
||||
- 重构:移动 "description" 属性到 ChainNode
|
||||
- 测试:添加 ChainConditionStringTest 测试
|
||||
|
||||
|
||||
|
||||
## v1.0.1 20250411
|
||||
- fix: LlmNode can not return the correct result if outType is text
|
||||
- fix: TextPromptTemplate can not parse `{{}}`
|
||||
|
||||
---
|
||||
- 修复:修复 LlmNode 当配置 outType 时,不能返回正确结果的问题
|
||||
- 修复:TextPromptTemplate 无法可以解析 `{{}}` 的问题
|
||||
|
||||
|
||||
|
||||
## v1.0.0 20250407
|
||||
- fix: fixed NodeContext.isUpstreamFullyExecuted() method
|
||||
- feat: add "concurrencyLimitSleepMillis" config for SparkLlm
|
||||
- feat: openai add chatPath config and embed path config
|
||||
- feat: HistoriesPrompt add temporaryMessages config
|
||||
|
||||
---
|
||||
- 修复:NodeContext.isUpstreamFullyExecuted() 方法判断错误的问题
|
||||
- 新增: SparkLlm 添加 concurrencyLimitSleepMillis 配置
|
||||
- 新增: openai 添加 chatPath 配置和 embed path 配置
|
||||
- 新增: HistoriesPrompt 添加 temporaryMessages 配置
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.9 20250331
|
||||
- feat: Added support for vector database Pgvector, thanks @daxian1218
|
||||
- feat: Chain added "SUSPEND" state and ChainSuspendListener listener
|
||||
- feat: Chain's RefType added "fixed" type.
|
||||
- feat: Chain's Parameter added "defaultValue"
|
||||
- feat: Chain added ChainResumeEvent event
|
||||
- feat: ChainNode added "awaitAsyncResult" property configuration
|
||||
- refactor: Return the complete response and answer information of coze chat to obtain complete information such as conversation_id, thanks @knowpigxia
|
||||
- refactor: Optimize the implementation details of RedisVectorStore
|
||||
- refactor: Chain removed OnErrorEvent and added ChainErrorListener instead
|
||||
- refactor: Rename BaseNode's "getParameters" method to "getParameterValues"
|
||||
- refactor: Rename Chain's event and remove the On prefix
|
||||
|
||||
---
|
||||
- 新增:新增向量数据库 Pgvector 的支持,感谢 @daxian1218
|
||||
- 新增:Chain 新增 "SUSPEND" 状态以及 ChainSuspendListener 监听
|
||||
- 新增:Chain 的 RefType 新增 "fixed" 类型。
|
||||
- 新增:Chain 的 Parameter 新增 "defaultValue"
|
||||
- 新增:Chain 新增 ChainResumeEvent 事件
|
||||
- 新增:ChainNode 新增 "awaitAsyncResult" 属性配置
|
||||
- 优化:返回 coze chat 完整的 response、answer 信息,以便获取 conversation_id 等完整信息,感谢 @knowpigxia
|
||||
- 优化:优化 RedisVectorStore 的实现细节
|
||||
- 优化:Chain 移除 OnErrorEvent 并新增 ChainErrorListener 代替
|
||||
- 优化:重命名 BaseNode 的 "getParameters" 方法为 "getParameterValues"
|
||||
- 优化:重命名 Chain 的事件,移除 On 前缀
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.8 20250318
|
||||
- feat: Added LLM support for siliconflow, thanks @daxian1218
|
||||
- feat: Chain's dynamic code node supports running Javascript scripts, thanks @hhongda
|
||||
- feat: Removed deepseek's invalid dependency on openai module, thanks @daxian1218
|
||||
- feat: Optimized EmbeddingModel and added direct embedding of String
|
||||
|
||||
---
|
||||
- 新增:新增 LLM 对 siliconflow(硅基流动)的支持,感谢 @daxian1218
|
||||
- 新增:Chain 的动态代码节点支持运行 Javascript 脚本,感谢 @hhongda
|
||||
- 优化:移除 deepseek 无效的依赖 openai 模块,感谢 @daxian1218
|
||||
- 优化:优化 EmbeddingModel,添加直接对 String 的 embed
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.7 20250312
|
||||
- feat: Added the tool of adding reasoning content to the return message, supporting deepseek's reasoning return, thanks @rirch
|
||||
- feat: Added support for vectorexdb embedded version, no need to deploy database separately, thanks @javpower
|
||||
- feat: Added support for accessing Tencent's large model language, Wensheng graph model and vectorization interface, thanks @sunchanghuilinqing
|
||||
- feat: Support for docking Doubao doubao-1-5-vision-pro-32k multimodal model and Wensheng graph, thanks @wang110wyy
|
||||
- feat: Added Wensheng graph model of Alibaba Bailian platform, thanks @sunchanghuilinqing
|
||||
- feat: Added VLLM-based large model access, thanks @sunchanghuilinqing
|
||||
- feat: Added LogUtil for log output
|
||||
- feat: Optimized the relevant code logic of DnjsonClient
|
||||
- fix: The problem of too long uid of Spark large model, thanks @wu-zhihao
|
||||
- fix: ChatStream of Ollama Llm An error occurred when actively closing the stream
|
||||
- fix: Fixed an issue where the endpoint configuration of OllamaProperties was incorrect by default
|
||||
|
||||
---
|
||||
- 新增:添加在在返回消息中增加推理内容的功能,支持 deepseek 的推理返回,感谢 @rirch
|
||||
- 新增:添加 vectorexdb 内嵌版本支持,无需额外部署数据库,感谢 @javpower
|
||||
- 新增:添加接入腾讯大模型语言、文生图模型与向量化接口的支持,感谢 @sunchanghuilinqing
|
||||
- 新增:对接豆包 doubao-1-5-vision-pro-32k 多模态模型以及文生图的支持,感谢 @wang110wyy
|
||||
- 新增:新增阿里百炼平台的文生图模型,感谢 @sunchanghuilinqing
|
||||
- 新增:新增基于 VLLM 部署大模型接入,感谢 @sunchanghuilinqing
|
||||
- 新增:新增 LogUtil 用于输出日志
|
||||
- 优化:优化 DnjsonClient 的相关代码逻辑
|
||||
- 修复:星火大模型的 uid 太长的问题,感谢 @wu-zhihao
|
||||
- 修复:Ollama Llm 的 chatStream 主动关闭流时发生错误的问题
|
||||
- 修复:修复默认情况下 OllamaProperties 的 endpoint 配置错误的问题
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.6 20250220
|
||||
- feat: Springboot's automatic configuration class for Ollama
|
||||
- feat: Added ToolPrompt tool to facilitate the use with Function Call
|
||||
- refactor: Change openAi to openAI
|
||||
- refactor: Optimize LlmNode and TextPromptTemplate
|
||||
- refactor: Upgrade related dependencies to the latest version
|
||||
- refactor: Optimize the empty user prompt words defined during the LlmNode runtime
|
||||
- refactor: Move the package name of tools to the directory chatModel (destructive update!!!)
|
||||
- refactor: Refactor InputParameter and OutputKey to merge into Parameter (destructive update!!!)
|
||||
- fix: Use the openai interface to connect to the local ollama to build a large model, and multiple tool definitions are called abnormally
|
||||
- fix: Fix the problem that easy-agents-bom cannot pull group code
|
||||
|
||||
---
|
||||
- 新增:Springboot 对 Ollama 的自动配置类
|
||||
- 新增:新增 ToolPrompt 功能,方便配合 Function Call 的使用
|
||||
- 优化:修改 openAi 为 openAI
|
||||
- 优化:优化 LlmNode 和 TextPromptTemplate
|
||||
- 优化:升级相关依赖到最新版本
|
||||
- 优化:优化 LlmNode 运行期定义空的用户提示词
|
||||
- 优化:移动 tools 的包名到目录 chatModel(破坏性更新 !!!)
|
||||
- 优化:重构 InputParameter 和 OutputKey 合并到 Parameter(破坏性更新 !!!)
|
||||
- 修复:使用 openai 接口对接本地 ollama 搭建大模型,多个函数定义调用异常
|
||||
- 修复:修复 easy-agents-bom 无法拉群代码的问题
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.5 20250210
|
||||
- feat: Added support for VectoRex vector database
|
||||
- feat: Added support for DeepSeek large models
|
||||
- feat: ImagePrompt adds support for local files, Stream and Base64 configurations
|
||||
- refactor: easy-agents-bom facilitates one-click import of all modules
|
||||
|
||||
---
|
||||
- 新增:添加 VectoRex 向量数据库的支持
|
||||
- 新增:增加 DeepSeek 大模型的支持
|
||||
- 新增:ImagePrompt 添加本地文件、Stream 和 Base64 配置的支持
|
||||
- 优化:easy-agents-bom 方便一键导入所有模块
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.4 20241230
|
||||
- refactor: Use pom to build and only manage versions
|
||||
- refactor: Optimize the relevant code of RedisVectorStore
|
||||
- refactor: BaseNode.getChainParameters() method
|
||||
- refactor: Optimize Chain.executeForResult method
|
||||
|
||||
---
|
||||
- 优化: 采用 pom方式构建并只做版本统一管理
|
||||
- 优化: 优化 RedisVectorStore 的相关代码
|
||||
- 优化: BaseNode.getChainParameters() 方法
|
||||
- 优化: 优化 Chain.executeForResult 方法
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.3 20241126
|
||||
- refactor: optimize Chain.executeForResult() method
|
||||
- refactor: optimize Chain events
|
||||
- fix: fixed Spark payload build error
|
||||
- fix: fixed qwen model unable to embed
|
||||
|
||||
---
|
||||
- 优化: 优化 Chain.executeForResult() 方法
|
||||
- 优化: 优化 Chain 的相关 event 事件
|
||||
- 修复: 修复星火大模型 payload 构建错误
|
||||
- 修复: 修复 qwen 大模型无法使用 Embedding 的问题
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.2 20241118
|
||||
- feat: Gitee AI adds support for Function Calling
|
||||
- feat: HumanMessage adds support for toolChoice configuration
|
||||
- refactor: Optimize editing node BaseNode and Maps tool classes
|
||||
|
||||
---
|
||||
- 新增: Gitee AI 添加对 Function Calling 的支持
|
||||
- 新增: HumanMessage 添加 toolChoice 配置的支持
|
||||
- 优化: 优化编辑节点 BaseNode 和 Maps 工具类
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.1 20241106
|
||||
- refactor: add BaseFunction.java
|
||||
- fix: spark LLM can not support v4.0
|
||||
- fix: fix code node can not get the parameters
|
||||
|
||||
---
|
||||
- 优化:新增 BaseFunction 类
|
||||
- 修复:修复星火大模型不支持 v4.0 的问题
|
||||
- 修复:修复代码节点无法获取参数的问题
|
||||
|
||||
|
||||
|
||||
## v1.0.0-rc.0 20241104
|
||||
- refactor: refactor chatModel apis
|
||||
- refactor: refactor chain and nodes
|
||||
- refactor: optimize easy-agents-solon-plugin @noear_admin
|
||||
|
||||
---
|
||||
- 优化:重构 chatModel api
|
||||
- 优化:重构 chain 链路 及其相关节点
|
||||
- 优化:优化 easy-agents-solon-plugin @noear_admin
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.13 20241026
|
||||
- feat: add plugin for solon framework
|
||||
- refactor: optimize VectorStore delete methods
|
||||
- refactor: optimize RedisVectorStore for sort by desc
|
||||
- refactor: optimize SparkLLM embedding
|
||||
|
||||
---
|
||||
- 新增:添加 solon 添加新的插件支持
|
||||
- 优化: 重构 VectorStore 的 delete 方法
|
||||
- 优化: 优化 RedisVectorStore 的搜索排序
|
||||
- 优化: 星火大模型新增秒级并发超过授权路数限制进行重试
|
||||
|
||||
|
||||
## v1.0.0-beta.12 20241025
|
||||
- refactor:add DocumentStoreConfig
|
||||
- refactor:optimize HistoriesPrompt.java
|
||||
- refactor: update pom.xml in easy-agents-bom
|
||||
- refactor: upgrade jedis version to "5.2.0"
|
||||
- refactor: optimize RedisVectorStore
|
||||
- fix: NoClassDefFoundError in jdk17: javax/xml/bind/DatatypeConverter 感谢 @songyinyin #I9AELG
|
||||
|
||||
---
|
||||
- 优化:添加 DocumentStoreConfig,向量数据库的配置都实现 DocumentStoreConfig
|
||||
- 优化:重构优化 HistoriesPrompt,使其支持更多的属性配置
|
||||
- 优化:更新 easy-agents-bom 的 pom.xml
|
||||
- 优化:升级 jedis 版本为 "5.2.0"
|
||||
- 优化:重构 RedisVectorStore 的错误信息,使之错误信息更加友好
|
||||
- 修复:修复 jdk17 下出现 NoSuchMethodError 问题,感谢 @songyinyin #I9AELG
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.11 20240918
|
||||
- feat: GenerateImageRequest add negativePrompt property
|
||||
- feat: Maps Util add putOrDefault method
|
||||
- feat: add siliconFlow image models
|
||||
- feat: ChatOptions add "seed" property
|
||||
- feat: Maps can put a child map by key
|
||||
- feat: Ollama add options config
|
||||
- feat: Ollama tool calling support
|
||||
- feat: add StringUtil.isJsonObject method
|
||||
- refactor: BaseImageRequest add extend options property
|
||||
- refactor: make ImagePrompt to extends HumanMessage
|
||||
- refactor: ImageResponse add error flag and errorMessage properties
|
||||
- refactor: rename Image.writeBytesToFile to writeToFile
|
||||
- refactor: rename "giteesd3" to "gitee"
|
||||
- refactor: optimize VectorData.toString
|
||||
|
||||
|
||||
---
|
||||
- 新增:GenerateImageRequest 添加反向提示词相关属性
|
||||
- 新增:Maps 工具类添加 putOrDefault 方法
|
||||
- 新增:添加 siliconFlow 的图片模型的支持
|
||||
- 新增: ChatOptions 添加 "seed" 属性
|
||||
- 新增:Maps 可以 put 一个子 map 的功能
|
||||
- 新增:新增 Ollama 的函数调用(Function Calling)的支持
|
||||
- 新增:添加 StringUtil.isJsonObject 方法
|
||||
- 优化:重构 BaseImageRequest 类,添加 options 属性
|
||||
- 优化:重构 ImagePrompt 使之继承于 HumanMessage
|
||||
- 优化:重构 ImageResponse 类,添加 error 和 errorMessage 属性
|
||||
- 优化:修改 Image.writeBytesToFile 方法为 writeToFile
|
||||
- 优化:重命名 "giteesd3" 为 "gitee"
|
||||
- 优化:重构 VectorData.toString 方法
|
||||
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.10 20240909
|
||||
- feat: Added support for RedisStore vector storage, thanks to @giteeClass
|
||||
- feat: Added support for large model dialogues for Coze Bot, thanks to @yulongsheng
|
||||
- feat: Automatic configuration of Springboot for ElasticSearch Store, thanks to @songyinyin
|
||||
- feat: Added support for Embedding of Tongyi Qianwen, thanks to @sssllg
|
||||
- feat: Added support for all text generation models of Gitee AI's serverless
|
||||
- feat: Added support for all image generation models of Gitee AI's serverless
|
||||
- docs: Corrected sample code errors in the documentation
|
||||
|
||||
---
|
||||
- 新增:添加 RedisStore 的向量存储支持,感谢 @giteeClass
|
||||
- 新增:新增 Coze Bot 的大模型对话支持,感谢 @yulongsheng
|
||||
- 新增: ElasticSearch Store 对 Springboot 的自动配置功能,感谢@songyinyin
|
||||
- 新增:新增通义千问的 Embedding 支持,感谢 @sssllg
|
||||
- 新增:新增对 Gitee AI 的 serverless 所有文本生成模型的支持
|
||||
- 新增:新增对 Gitee AI 的 serverless 所有图片生成模型的支持
|
||||
- 文档:修正文档的示例代码错误
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.9 20240813
|
||||
- feat: add custom request header in openaiLLM https://github.com/easy-agents/easy-agents/issues/5
|
||||
- feat: add https.proxyHost config for the http client, close https://github.com/easy-agents/easy-agents/issues/1
|
||||
- feat: add SpringBoot3 auto config support @songyinyin
|
||||
- feat: add openSearch store support @songyinyin
|
||||
- fix: fix config error in QwenAutoConfiguration @songyinyin
|
||||
- fix: NPE in OpenAILLmUtil.promptToEmbeddingsPayload
|
||||
- fix: fix FunctionMessageResponse error in BaseLlmClientListener, @imayou
|
||||
- refactor: update bom module
|
||||
- refactor: optimize SparkLlm.java
|
||||
|
||||
---
|
||||
- 新增: 添加自定义 openaiLLM 请求 api 的支持 https://github.com/easy-agents/easy-agents/issues/5
|
||||
- 新增: 添加 https.proxyHost 配置的支持 https://github.com/easy-agents/easy-agents/issues/1
|
||||
- 新增: 添加对 SpringBoot3 自动配置的支持 @songyinyin
|
||||
- 新增: 添加使用 openSearch 用于向量数据存储的支持 @songyinyin
|
||||
- 修复: 修复 QwenAutoConfiguration 配置错误的问题 @songyinyin
|
||||
- 修复: 修复 OpenAILLmUtil.promptToEmbeddingsPayload 空指针异常的问题
|
||||
- 修复: 修复 FunctionMessageResponse 在某些情况下出错的问题, @imayou
|
||||
- 优化: 更新重构 bom 模块
|
||||
- 优化: 优化 SparkLlm.java 的相关代码
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.8 20240714
|
||||
- feat: add "async" flag for the ChainNode
|
||||
- feat: add Ollama LLM
|
||||
- feat: add DnjsonClient for OllamaLlm
|
||||
- refactor: refactor ChainCondition.java
|
||||
- refactor: add throw LlmException if LLMs has error
|
||||
- refactor: refactor DocumentParser
|
||||
- refactor: refactor chain module
|
||||
- refactor: rename GroovyExecNode.java and QLExpressExecNode.java
|
||||
- refactor: add children property in Parameter
|
||||
- refactor: remove unused code AsyncHttpClient.java
|
||||
- refactor: use LlmException to replace LLMClientException
|
||||
fix: Milvus type mismatch for filed 'id'
|
||||
- test: add LoopChain test
|
||||
- test: add ollama test use openai instance
|
||||
- docs: add Japanese README
|
||||
|
||||
---
|
||||
- 新增:为 ChainNode 添加 "async" 属性标识的设置
|
||||
- 新增:添加基于 Ollama 大语言模型的对接,非 openai 适配模式
|
||||
- 新增:新增 DnjsonClient 用于和 Ollama 的 stream 模型对接
|
||||
- 优化:重构 ChainCondition
|
||||
- 优化:chat 时当大语言模型发生错误时抛出异常,之前返回 null
|
||||
- 优化:重构 DocumentParser
|
||||
- 优化:Parameter 支持子参数的配置能力
|
||||
- 修复:Milvus 向量数据库当传入 number 类型是出错的问题
|
||||
- 测试:添加对 LoopChain 的测试
|
||||
- 测试:添加文使用 openai 兼容 api 对 Ollama 对接的测试
|
||||
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.7 20240705
|
||||
- feat: add image models support
|
||||
- feat: add SimpleTokenizeSplitter
|
||||
- feat: add OmniParseDocumentParser
|
||||
- feat: add openai,stability AI and gitee-sd3 AI support
|
||||
- feat: add moonshot support
|
||||
- feat: add chain dsl support
|
||||
- refactor: optimize chatModel clients
|
||||
- refactor: optimize SparkLLM
|
||||
- refactor: optimize slf4j dependencies
|
||||
- refactor: optimize Agent define
|
||||
- refactor: optimize chain
|
||||
- test: add .pdf and .doc parse test
|
||||
- test: add SimpleDocumentSplitterTest.java
|
||||
|
||||
---
|
||||
- 新增:新增图片模型的支持
|
||||
- 新增:新增 SimpleTokenizeSplitter 分割器
|
||||
- 新增:新增 OmniParseDocumentParser 文档解析器
|
||||
- 新增:新增 openai、stability ai 以及 gitee ai 对图片生成的支持
|
||||
- 新增:新增月之暗面的支持
|
||||
- 优化:优化 chatModel 客户端的细节
|
||||
- 优化:优化星火大模型的细节
|
||||
- 优化:优化 slf4j 依赖的细节
|
||||
- 优化:优化 Agent 和 Chain 的定义细节
|
||||
- 测试:添加 .pdf 和 .doc 的解析测试
|
||||
- 测试:添加文档分割器的测试
|
||||
- 测试:添加 token 文档分割器的测试
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.5 20240617
|
||||
- feat: add ImagePrompt to send image to LLM
|
||||
- feat: chatOptions add topP/topK and stop config
|
||||
- refactor: rename TextMessage.java to AbstractTextMessage.java
|
||||
- refactor: refactor chatModel methods
|
||||
- refactor: refactor FunctionMessageResponse.java
|
||||
- refactor: optimize HttpClient.java And SseClient.java
|
||||
- fix: fix tool calling error in QwenLLM
|
||||
- test: add chat with image test
|
||||
|
||||
|
||||
---
|
||||
- 新增:新增 ImagePrompt 用于发送图片对话的场景
|
||||
- 新增:对话模型下的 ChatOptions 添加 topK 和 topP 配置的支持
|
||||
- 优化:重命名 TextMessage 为 AbstractTextMessage
|
||||
- 优化:重构 LLM 的方法定义,使之更加简单易用
|
||||
- 优化:优化 HttpClient.java 和 SseClient.java 的相关代码
|
||||
- 修复:通义千问 QwenLLM 在 tool calling 下无法正常调用的问题
|
||||
- 测试:添加发送图片相关的测试内容
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.4 20240531
|
||||
- feat: OpenAILlm support embedding model config
|
||||
- feat: add get dimensions method in EmbeddingModel
|
||||
- feat: SparkLlm support embedding
|
||||
- feat: optimize MilvusVectorStore
|
||||
- feat: MilvusVectorStore add username and password config
|
||||
- refactor: optimize HttpClient.java
|
||||
- refactor: optimize AliyunVectorStore
|
||||
- refactor: update StoreOptions to extends Metadata
|
||||
- refactor: optimize StoreResult and Metadata
|
||||
- fix: fix AIMessage tokens parse
|
||||
|
||||
|
||||
---
|
||||
- 新增:OpenAILlm 添加自定义 embedding 模型的支持
|
||||
- 新增:EmbeddingModel 添加获取向量维度的支持
|
||||
- 新增:SparkLlm 星火大模型添加对 embedding 的支持
|
||||
- 新增:添加 Milvus 向量数据库的支持
|
||||
- 优化:优化 HttpClient 的代码
|
||||
- 优化:优化 AliyunVectorStore 的代码
|
||||
- 优化:优化 StoreOptions 和 Metadata 的代码
|
||||
- 修复:AIMessage 的 tokens 消耗解析不正确的问题
|
||||
|
||||
|
||||
|
||||
|
||||
## v1.0.0-beta.3 20240516
|
||||
- feat:add "description" to agent for automatic arrangement by LLM
|
||||
- feat: StoreResult can return ids if document store success
|
||||
- feat: StoreOptions support set multi partitionNames
|
||||
- feat: add DocumentIdGenerator for Document and Store
|
||||
- feat: add ChainException for Chain.executeForResult
|
||||
- refactor: rename "SimplePrompt" to "TextPrompt"
|
||||
|
||||
---
|
||||
- 新增:为 Agent 添加 description 属性,方便用于 AI 自动编排的场景
|
||||
- 新增:Agent 添加对 outputDefs 的定义支持
|
||||
- 新增:添加 DocumentIdGenerator 用于在对文档存储时自动生成 id 的功能
|
||||
- 新增:StoreOptions 添加多个 partitionName 配置的支持
|
||||
- 新增:当 Document 保存成功时,自动返回保存的 id
|
||||
- 优化:Chain.executeForResult 会抛出异常 ChainException
|
||||
- 修复:ChatGLM 的 Chat JSON 解析错误的问题
|
||||
- 测试:优化 SparkLlmTest 的测试代码
|
||||
- 文档:完善基础文档
|
||||
232
easy-agents-bom/pom.xml
Normal file
232
easy-agents-bom/pom.xml
Normal file
@@ -0,0 +1,232 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-bom</name>
|
||||
<artifactId>easy-agents-bom</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-core</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-common</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!--image model start-->
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-gitee</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-qianfan</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-qwen</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-siliconflow</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-stability</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-tencent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-image-volcengine</artifactId>
|
||||
</dependency>
|
||||
<!--image model end-->
|
||||
|
||||
|
||||
<!--chatModel start-->
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat-deepseek</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat-ollama</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat-qwen</artifactId>
|
||||
</dependency>
|
||||
<!--chatModel end-->
|
||||
|
||||
|
||||
<!--embedding start-->
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-embedding-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-embedding-ollama</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-embedding-qwen</artifactId>
|
||||
</dependency>
|
||||
<!--embedding end-->
|
||||
|
||||
|
||||
<!--rerank start-->
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-rerank-default</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-rerank-gitee</artifactId>
|
||||
</dependency>
|
||||
<!--rerank end-->
|
||||
|
||||
<!--store start-->
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-aliyun</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-chroma</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-elasticsearch</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-opensearch</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-pgvector</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-qcloud</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-qdrant</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-vectorex</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-store-vectorexdb</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!--store end-->
|
||||
|
||||
<!--search engines start-->
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-search-engine-service</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-search-engine-es</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-search-engine-lucene</artifactId>
|
||||
</dependency>
|
||||
<!--search engines end-->
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
30
easy-agents-chat/easy-agents-chat-deepseek/pom.xml
Normal file
30
easy-agents-chat/easy-agents-chat-deepseek/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
<name>easy-agents-chat-deepseek</name>
|
||||
<artifactId>easy-agents-chat-deepseek</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<DeepseekConfig> {
|
||||
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,不使用实例级拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
*/
|
||||
public DeepseekChatModel(DeepseekConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,并指定实例级拦截器。
|
||||
* <p>
|
||||
* 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
|
||||
* 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
* @param userInterceptors 实例级拦截器列表
|
||||
*/
|
||||
public DeepseekChatModel(DeepseekConfig config, List<ChatInterceptor> userInterceptors) {
|
||||
super(config, userInterceptors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
34
easy-agents-chat/easy-agents-chat-ollama/pom.xml
Normal file
34
easy-agents-chat/easy-agents-chat-ollama/pom.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-chat-ollama</name>
|
||||
<artifactId>easy-agents-chat-ollama</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<OllamaChatConfig> {
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,不使用实例级拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
*/
|
||||
public OllamaChatModel(OllamaChatConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,并指定实例级拦截器。
|
||||
* <p>
|
||||
* 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
|
||||
* 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
* @param userInterceptors 实例级拦截器列表
|
||||
*/
|
||||
public OllamaChatModel(OllamaChatConfig config, List<ChatInterceptor> userInterceptors) {
|
||||
super(config, userInterceptors);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ChatRequestSpecBuilder getChatRequestSpecBuilder() {
|
||||
return new OllamaRequestSpecBuilder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
32
easy-agents-chat/easy-agents-chat-openai/pom.xml
Normal file
32
easy-agents-chat/easy-agents-chat-openai/pom.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-chat-openai</name>
|
||||
<artifactId>easy-agents-chat-openai</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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}。
|
||||
* <p>
|
||||
* 默认值:
|
||||
* <ul>
|
||||
* <li>provider: {@code "openai"}</li>
|
||||
* <li>model: {@code "gpt-3.5-turbo"}</li>
|
||||
* <li>endpoint: {@code "https://api.openai.com"}</li>
|
||||
* <li>requestPath: {@code "/v1/chat/completions"}</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 该配置类专为 OpenAI 兼容 API 设计,适用于 OpenAI 官方、Azure OpenAI 或其他兼容服务。
|
||||
*/
|
||||
public class OpenAIChatConfig extends ChatConfig {
|
||||
|
||||
private static final String DEFAULT_PROVIDER = "openai";
|
||||
private static final String DEFAULT_MODEL = "gpt-3.5-turbo";
|
||||
private static final String DEFAULT_ENDPOINT = "https://api.openai.com";
|
||||
private static final String DEFAULT_REQUEST_PATH = "/v1/chat/completions";
|
||||
|
||||
public OpenAIChatConfig() {
|
||||
setProvider(DEFAULT_PROVIDER);
|
||||
setEndpoint(DEFAULT_ENDPOINT);
|
||||
setRequestPath(DEFAULT_REQUEST_PATH);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个 {@link OpenAIChatModel} 实例,使用当前配置。
|
||||
*
|
||||
* @return 新的 {@link OpenAIChatModel} 实例
|
||||
*/
|
||||
public final OpenAIChatModel toChatModel() {
|
||||
return new OpenAIChatModel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个 {@link OpenAIChatModel} 实例,使用当前配置和指定的实例级拦截器。
|
||||
*
|
||||
* @param interceptors 实例级拦截器列表,可为 {@code null} 或空列表
|
||||
* @return 新的 {@link OpenAIChatModel} 实例
|
||||
*/
|
||||
public final OpenAIChatModel toChatModel(List<ChatInterceptor> interceptors) {
|
||||
return new OpenAIChatModel(this, interceptors);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建器类,用于流畅地创建 {@link OpenAIChatConfig} 或直接构建 {@link OpenAIChatModel}。
|
||||
*/
|
||||
public static class Builder {
|
||||
private final OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
|
||||
// --- BaseModelConfig fields ---
|
||||
|
||||
public Builder apiKey(String apiKey) {
|
||||
config.setApiKey(apiKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder provider(String provider) {
|
||||
config.setProvider(provider);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder endpoint(String endpoint) {
|
||||
config.setEndpoint(endpoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder requestPath(String requestPath) {
|
||||
config.setRequestPath(requestPath);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder model(String model) {
|
||||
config.setModel(model);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个自定义属性(会进行深拷贝,不会持有外部引用)。
|
||||
*/
|
||||
public Builder customProperty(String key, Object value) {
|
||||
config.putCustomProperty(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义属性映射(会进行深拷贝,不会持有外部 map 引用)。
|
||||
*/
|
||||
public Builder customProperties(Map<String, Object> customProperties) {
|
||||
config.setCustomProperties(customProperties);
|
||||
return this;
|
||||
}
|
||||
|
||||
// --- ChatConfig fields ---
|
||||
|
||||
public Builder supportImage(Boolean supportImage) {
|
||||
config.setSupportImage(supportImage);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder supportImageBase64Only(Boolean supportImageBase64Only) {
|
||||
config.setSupportImageBase64Only(supportImageBase64Only);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder supportAudio(Boolean supportAudio) {
|
||||
config.setSupportAudio(supportAudio);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder supportVideo(Boolean supportVideo) {
|
||||
config.setSupportVideo(supportVideo);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder supportTool(Boolean supportTool) {
|
||||
config.setSupportTool(supportTool);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder supportThinking(Boolean supportThinking) {
|
||||
config.setSupportThinking(supportThinking);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder thinkingEnabled(boolean thinkingEnabled) {
|
||||
config.setThinkingEnabled(thinkingEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder observabilityEnabled(boolean observabilityEnabled) {
|
||||
config.setObservabilityEnabled(observabilityEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder logEnabled(boolean logEnabled) {
|
||||
config.setLogEnabled(logEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建 {@link OpenAIChatConfig} 配置对象。
|
||||
* <p>
|
||||
* 该方法会校验必要字段(如 {@code apiKey}),若缺失将抛出异常。
|
||||
*
|
||||
* @return 构建完成的配置对象
|
||||
* @throws IllegalStateException 如果 {@code apiKey} 未设置或为空
|
||||
*/
|
||||
public OpenAIChatConfig build() {
|
||||
if (StringUtil.noText(config.getApiKey())) {
|
||||
throw new IllegalStateException("apiKey must be set for OpenAIChatConfig");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接构建 {@link OpenAIChatModel} 实例,使用默认(全局)拦截器。
|
||||
*
|
||||
* @return 新的聊天模型实例
|
||||
* @throws IllegalStateException 如果 {@code apiKey} 未设置或为空
|
||||
*/
|
||||
public OpenAIChatModel buildModel() {
|
||||
return new OpenAIChatModel(build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接构建 {@link OpenAIChatModel} 实例,并指定实例级拦截器。
|
||||
*
|
||||
* @param interceptors 实例级拦截器列表,可为 {@code null} 或空
|
||||
* @return 新的聊天模型实例
|
||||
* @throws IllegalStateException 如果 {@code apiKey} 未设置或为空
|
||||
*/
|
||||
public OpenAIChatModel buildModel(List<ChatInterceptor> interceptors) {
|
||||
return new OpenAIChatModel(build(), interceptors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个新的构建器实例,用于链式配置。
|
||||
*
|
||||
* @return {@link Builder} 实例
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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 聊天模型实现。
|
||||
* <p>
|
||||
* 该类封装了 OpenAI API 的具体调用细节,包括:
|
||||
* <ul>
|
||||
* <li>请求体构建(支持同步/流式)</li>
|
||||
* <li>HTTP 客户端管理</li>
|
||||
* <li>解析器配置(同步/流式使用不同解析器)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 所有横切逻辑(监控、日志、拦截)由 {@link BaseChatModel} 的责任链处理,
|
||||
* 本类只关注 OpenAI 协议特有的实现细节。
|
||||
*/
|
||||
public class OpenAIChatModel extends OpenAICompatibleChatModel<OpenAIChatConfig> {
|
||||
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,不使用实例级拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
*/
|
||||
public OpenAIChatModel(OpenAIChatConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,并指定实例级拦截器。
|
||||
* <p>
|
||||
* 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
|
||||
* 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
* @param userInterceptors 实例级拦截器列表
|
||||
*/
|
||||
public OpenAIChatModel(OpenAIChatConfig config, List<ChatInterceptor> userInterceptors) {
|
||||
super(config, userInterceptors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.easyagents.llm.openai;
|
||||
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.easyagents.core.model.chat.StreamResponseListener;
|
||||
import com.easyagents.core.model.chat.response.AiMessageResponse;
|
||||
import com.easyagents.core.model.client.StreamContext;
|
||||
import com.easyagents.core.prompt.Prompt;
|
||||
import com.easyagents.core.prompt.SimplePrompt;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ChatModelTestUtils {
|
||||
|
||||
public static void waitForStream(
|
||||
ChatModel model,
|
||||
String prompt,
|
||||
StreamResponseListener listener) {
|
||||
waitForStream(model, new SimplePrompt(prompt), listener, Integer.MAX_VALUE, null);
|
||||
}
|
||||
|
||||
public static void waitForStream(
|
||||
ChatModel model,
|
||||
String prompt,
|
||||
StreamResponseListener listener,
|
||||
ChatOptions options) {
|
||||
waitForStream(model, new SimplePrompt(prompt), listener, Integer.MAX_VALUE, options);
|
||||
}
|
||||
|
||||
public static void waitForStream(
|
||||
ChatModel model,
|
||||
Prompt prompt,
|
||||
StreamResponseListener listener) {
|
||||
waitForStream(model, prompt, listener, Integer.MAX_VALUE, null);
|
||||
}
|
||||
|
||||
public static void waitForStream(
|
||||
ChatModel model,
|
||||
Prompt prompt,
|
||||
StreamResponseListener listener,
|
||||
ChatOptions options) {
|
||||
waitForStream(model, prompt, listener, Integer.MAX_VALUE, options);
|
||||
}
|
||||
|
||||
public static void waitForStream(
|
||||
ChatModel model,
|
||||
Prompt prompt,
|
||||
StreamResponseListener listener,
|
||||
long timeoutSeconds, ChatOptions options) {
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
StreamResponseListener wrapped = new StreamResponseListener() {
|
||||
@Override
|
||||
public void onStart(StreamContext context) {
|
||||
listener.onStart(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(StreamContext ctx, AiMessageResponse resp) {
|
||||
listener.onMessage(ctx, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(StreamContext ctx) {
|
||||
listener.onStop(ctx);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(StreamContext context, Throwable throwable) {
|
||||
listener.onFailure(context, throwable);
|
||||
}
|
||||
};
|
||||
|
||||
model.chatStream(prompt, wrapped, options);
|
||||
try {
|
||||
if (!latch.await(timeoutSeconds, TimeUnit.SECONDS)) {
|
||||
throw new RuntimeException("Stream did not complete within " + timeoutSeconds + "s");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.easyagents.llm.openai;
|
||||
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.response.AiMessageResponse;
|
||||
import com.easyagents.core.prompt.SimplePrompt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
|
||||
public class GiteeAiImageTest {
|
||||
|
||||
|
||||
@NotNull
|
||||
private static OpenAIChatConfig getOpenAIChatConfig() {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setApiKey("PXW1GXE******L7D12");
|
||||
// config.setModel("InternVL3-78B");
|
||||
config.setModel("Qwen3-32B");
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setLogEnabled(true);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImage() {
|
||||
OpenAIChatConfig config = getOpenAIChatConfig();
|
||||
ChatModel chatModel = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("请识别并输入 markdown,请用中文输出");
|
||||
prompt.addImageUrl("http://www.codeformat.cn/static/images/logo.png");
|
||||
|
||||
AiMessageResponse response = chatModel.chat(prompt);
|
||||
if (!response.isError()) {
|
||||
System.out.println(response.getMessage().getContent());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChat() {
|
||||
OpenAIChatConfig config = getOpenAIChatConfig();
|
||||
config.setSupportImage(false);
|
||||
ChatModel chatModel = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("你叫什么名字");
|
||||
prompt.addImageUrl("http://www.codeformat.cn/static/images/logo.png");
|
||||
|
||||
AiMessageResponse response = chatModel.chat(prompt);
|
||||
if (!response.isError()) {
|
||||
System.out.println(response.getMessage().getContent());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
package com.easyagents.llm.openai;
|
||||
|
||||
import com.easyagents.core.agent.react.ReActAgent;
|
||||
import com.easyagents.core.agent.react.ReActAgentListener;
|
||||
import com.easyagents.core.agent.react.ReActAgentState;
|
||||
import com.easyagents.core.agent.react.ReActStep;
|
||||
import com.easyagents.core.memory.ChatMemory;
|
||||
import com.easyagents.core.message.ToolCall;
|
||||
import com.easyagents.core.message.ToolMessage;
|
||||
import com.easyagents.core.message.UserMessage;
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.easyagents.core.model.chat.StreamResponseListener;
|
||||
import com.easyagents.core.model.chat.response.AiMessageResponse;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.core.model.chat.tool.ToolScanner;
|
||||
import com.easyagents.core.model.client.StreamContext;
|
||||
import com.easyagents.core.model.exception.ModelException;
|
||||
import com.easyagents.core.prompt.SimplePrompt;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class OpenAIChatModelTest {
|
||||
|
||||
@Test(expected = ModelException.class)
|
||||
public void testChat() {
|
||||
|
||||
String output = OpenAIChatConfig.builder()
|
||||
.endpoint("https://ai.gitee.com")
|
||||
.provider("GiteeAI")
|
||||
.model("Qwen3-32B")
|
||||
.apiKey("PXW1****D12")
|
||||
.buildModel()
|
||||
.chat("你叫什么名字");
|
||||
|
||||
System.out.println(output);
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testChatStream() {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setApiKey("PXW1GXE***");
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setModel("Qwen3-32B");
|
||||
config.setLogEnabled(true);
|
||||
|
||||
ChatOptions options = ChatOptions.builder().thinkingEnabled(false).build();
|
||||
|
||||
ChatModel chatModel = new OpenAIChatModel(config);
|
||||
|
||||
ChatModelTestUtils.waitForStream(chatModel, "你叫什么名字", new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
System.out.println(response.getMessage().getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(StreamContext context, Throwable throwable) {
|
||||
System.out.println("onFailure>>>>" + throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(StreamContext context) {
|
||||
System.out.println("stop!!!!");
|
||||
}
|
||||
}, options);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testChatStreamBailian() {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setApiKey("sk-32ab******57502");
|
||||
config.setEndpoint("https://dashscope.aliyuncs.com");
|
||||
config.setRequestPath("/compatible-mode/v1/chat/completions");
|
||||
config.setModel("qwen3-max");
|
||||
ChatModel chatModel = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("北京的天气如何?");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
|
||||
ChatModelTestUtils.waitForStream(chatModel, prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onFailure(StreamContext context, Throwable throwable) {
|
||||
System.out.println("onFailure>>>>" + throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
if (response.getMessage().getContent() == null) {
|
||||
System.out.println(response.getMessage());
|
||||
}
|
||||
|
||||
if (response.getMessage().isFinalDelta()) {
|
||||
List<ToolCall> toolCalls = response.getMessage().getToolCalls();
|
||||
System.out.println(toolCalls);
|
||||
}
|
||||
|
||||
System.out.println("onMessage >>>>>" + response.getMessage().getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(StreamContext context) {
|
||||
System.out.println("stop!!!!");
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChatOllama() {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setEndpoint("http://localhost:11434");
|
||||
config.setModel("llama3");
|
||||
config.setLogEnabled(true);
|
||||
|
||||
ChatModel chatModel = new OpenAIChatModel(config);
|
||||
chatModel.chatStream("who are you", new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
System.out.println(response.getMessage().getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(StreamContext context) {
|
||||
System.out.println("stop!!!!");
|
||||
}
|
||||
});
|
||||
|
||||
// try {
|
||||
// Thread.sleep(2000);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testChatWithImage() {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setApiKey("sk-5gqOcl*****");
|
||||
config.setModel("gpt-4-turbo");
|
||||
|
||||
|
||||
ChatModel chatModel = new OpenAIChatModel(config);
|
||||
SimplePrompt prompt = new SimplePrompt("What's in this image?");
|
||||
prompt.addImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg");
|
||||
|
||||
|
||||
AiMessageResponse response = chatModel.chat(prompt);
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling1() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setApiKey("sk-rts5NF6n*******");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
AiMessageResponse response = llm.chat(prompt);
|
||||
|
||||
System.out.println(response.executeToolCallsAndGetResults());
|
||||
// 阴转多云
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling2() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setApiKey("sk-rts5NF6n*******");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
AiMessageResponse response = llm.chat(prompt);
|
||||
|
||||
if (response.hasToolCalls()) {
|
||||
prompt.setToolMessages(response.executeToolCallsAndGetToolMessages());
|
||||
AiMessageResponse response1 = llm.chat(prompt);
|
||||
System.out.println(response1.getMessage().getContent());
|
||||
} else {
|
||||
System.out.println(response);
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling3() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ark.cn-beijing.volces.com");
|
||||
config.setRequestPath("/api/v3/chat/completions");
|
||||
config.setModel("deepseek-v3-250324");
|
||||
config.setApiKey("2d57a");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
AiMessageResponse response = llm.chat(prompt);
|
||||
|
||||
if (response.hasToolCalls()) {
|
||||
prompt.setToolMessages(response.executeToolCallsAndGetToolMessages());
|
||||
AiMessageResponse response1 = llm.chat(prompt);
|
||||
System.out.println(response1.getMessage().getContent());
|
||||
} else {
|
||||
System.out.println(response);
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling4() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ark.cn-beijing.volces.com");
|
||||
config.setRequestPath("/api/v3/chat/completions");
|
||||
config.setModel("deepseek-v3-250324");
|
||||
config.setApiKey("2d57aa75");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
System.out.println(" onMessage >>>>>" + response.hasToolCalls());
|
||||
}
|
||||
});
|
||||
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling44() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ark.cn-beijing.volces.com");
|
||||
config.setRequestPath("/api/v3/chat/completions");
|
||||
config.setModel("deepseek-v3-250324");
|
||||
config.setApiKey("2d5");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
System.out.println(" onMessage >>>>>" + response.hasToolCalls());
|
||||
}
|
||||
});
|
||||
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling444() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
// config.setRequestPath("/api/v3/chat/completions");
|
||||
// config.setModel("Qwen3-32B");
|
||||
config.setModel("DeepSeek-V3.2");
|
||||
config.setApiKey("PXW1G***L7D12");
|
||||
// config.setLogEnabled(false);
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("北京和上海的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
|
||||
// System.out.println("onMessage11 >>>>>" + response);
|
||||
if (response.getMessage().isFinalDelta() && response.hasToolCalls()) {
|
||||
System.out.println(":::::::: start....");
|
||||
List<ToolMessage> toolMessages = response.executeToolCallsAndGetToolMessages();
|
||||
prompt.setAiMessage(response.getMessage());
|
||||
prompt.setToolMessages(toolMessages);
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
String msg = response.getMessage().getContent() != null ? response.getMessage().getContent() : response.getMessage().getReasoningContent();
|
||||
System.out.println(":::22" + msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(StreamContext context) {
|
||||
System.out.println("onStop >>>>>");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
String msg = response.getMessage().getContent() != null ? response.getMessage().getContent() : response.getMessage().getReasoningContent();
|
||||
System.out.println(">>>" + msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TimeUnit.SECONDS.sleep(25);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling5() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setModel("Qwen3-32B");
|
||||
config.setApiKey("PXW1G*********D12");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("北京和上海的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
// System.out.println("onMessage >>>>>" + response);
|
||||
}
|
||||
});
|
||||
|
||||
TimeUnit.SECONDS.sleep(25);
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling55() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setModel("Qwen3-32B");
|
||||
// config.setModel("DeepSeek-V3");
|
||||
// config.setSupportToolMessage(false);
|
||||
config.setApiKey("PXW1");
|
||||
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("/no_think 北京和上海的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
|
||||
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
if (response.getMessage().isFinalDelta() && response.hasToolCalls()) {
|
||||
System.out.println(":::::::: start....");
|
||||
prompt.setAiMessage(response.getMessage());
|
||||
prompt.setToolMessages(response.executeToolCallsAndGetToolMessages());
|
||||
llm.chatStream(prompt, new StreamResponseListener() {
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
String msg = response.getMessage().getContent() != null ? response.getMessage().getContent() : response.getMessage().getReasoningContent();
|
||||
System.out.println(":::" + msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
String msg = response.getMessage().getContent() != null ? response.getMessage().getContent() : response.getMessage().getReasoningContent();
|
||||
System.out.println(">>>" + msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TimeUnit.SECONDS.sleep(25);
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void testFunctionCalling6() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
config.setLogEnabled(true);
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setModel("Qwen3-32B");
|
||||
config.setApiKey("PXW1");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("/no_think 北京和上海的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
AiMessageResponse response = llm.chat(prompt);
|
||||
|
||||
prompt.setToolMessages(response.executeToolCallsAndGetToolMessages());
|
||||
|
||||
System.out.println(llm.chat(prompt));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testReAct1() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
// config.setDebug(true);
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setModel("Qwen3-32B");
|
||||
config.setApiKey("****");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
List<Tool> tools = ToolScanner.scan(WeatherFunctions.class);
|
||||
// ReActAgent reActAgent = new ReActAgent(llm, functions, "北京和上海的天气怎么样?");
|
||||
ReActAgent reActAgent = new ReActAgent(llm, tools, "介绍一下北京");
|
||||
reActAgent.addListener(new ReActAgentListener() {
|
||||
|
||||
@Override
|
||||
public void onActionStart(ReActStep step) {
|
||||
System.out.println(">>>>>>" + step.getThought());
|
||||
System.out.println("正在调用工具 >>>>> " + step.getAction() + ":" + step.getActionInput());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionEnd(ReActStep step, Object result) {
|
||||
System.out.println("工具调用结束 >>>>> " + step.getAction() + ":" + step.getActionInput() + ">>>>结果:" + result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinalAnswer(String finalAnswer) {
|
||||
System.out.println("onFinalAnswer >>>>>" + finalAnswer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNonActionResponse(AiMessageResponse response) {
|
||||
System.out.println("onNonActionResponse >>>>>" + response.getMessage().getContent());
|
||||
}
|
||||
});
|
||||
|
||||
reActAgent.execute();
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testReAct2() throws InterruptedException {
|
||||
OpenAIChatConfig config = new OpenAIChatConfig();
|
||||
// config.setDebug(true);
|
||||
config.setEndpoint("https://ai.gitee.com");
|
||||
config.setModel("Qwen2-72B-Instruct");
|
||||
config.setApiKey("*****");
|
||||
|
||||
OpenAIChatModel llm = new OpenAIChatModel(config);
|
||||
|
||||
List<Tool> tools = ToolScanner.scan(WeatherFunctions.class);
|
||||
ReActAgent reActAgent = new ReActAgent(llm, tools, "今天的天气怎么样?");
|
||||
// reActAgent.setStreamable(true);
|
||||
reActAgent.addListener(new ReActAgentListener() {
|
||||
|
||||
@Override
|
||||
public void onChatResponseStream(StreamContext context, AiMessageResponse response) {
|
||||
// System.out.print(response.getMessage().getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestUserInput(String question) {
|
||||
System.out.println("onRequestUserInput>>>" + question);
|
||||
|
||||
ReActAgentState state = reActAgent.getState();
|
||||
state.addMessage(new UserMessage("我在北京市"));
|
||||
ReActAgent newAgent = new ReActAgent(llm, tools, state);
|
||||
newAgent.addListener(this);
|
||||
newAgent.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionStart(ReActStep step) {
|
||||
System.out.println(">>>>>>" + step.getThought());
|
||||
System.out.println("正在调用工具 >>>>> " + step.getAction() + ":" + step.getActionInput());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionEnd(ReActStep step, Object result) {
|
||||
System.out.println("工具调用结束 >>>>> " + step.getAction() + ":" + step.getActionInput() + ">>>>结果:" + result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinalAnswer(String finalAnswer) {
|
||||
System.out.println("onFinalAnswer >>>>>" + finalAnswer);
|
||||
ChatMemory memory = reActAgent.getMemoryPrompt().getMemory();
|
||||
System.out.println(memory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNonActionResponseStream(StreamContext context) {
|
||||
System.out.println("onNonActionResponseStream >>>>>" + context);
|
||||
}
|
||||
});
|
||||
|
||||
reActAgent.execute();
|
||||
|
||||
TimeUnit.SECONDS.sleep(20);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.easyagents.llm.openai;
|
||||
|
||||
import com.easyagents.core.model.chat.tool.annotation.ToolDef;
|
||||
import com.easyagents.core.model.chat.tool.annotation.ToolParam;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class WeatherFunctions {
|
||||
|
||||
private static final String[] weathers = {
|
||||
"晴", "多云", "阴", "小雨", "中雨", "大雨", "暴雨", "雷阵雨",
|
||||
"小雪", "中雪", "大雪", "暴雪", "雨夹雪", "雾", "霾", "沙尘暴",
|
||||
"冰雹", "阵雨", "冻雨", "晴间多云", "局部多云", "强对流"
|
||||
};
|
||||
|
||||
@ToolDef(name = "get_the_weather_info", description = "get the weather info")
|
||||
public static String getWeatherInfo(@ToolParam(name = "city", description = "the city name") String name) {
|
||||
String weather = weathers[ThreadLocalRandom.current().nextInt(weathers.length)];
|
||||
System.out.println(">>>>>>>>>>>>>>!!!!!!" + name + ":" + weather);
|
||||
return weather;
|
||||
}
|
||||
|
||||
}
|
||||
33
easy-agents-chat/easy-agents-chat-qwen/pom.xml
Normal file
33
easy-agents-chat/easy-agents-chat-qwen/pom.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-chat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-chat-qwen</name>
|
||||
<artifactId>easy-agents-chat-qwen</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<QwenChatConfig> {
|
||||
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,不使用实例级拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
*/
|
||||
public QwenChatModel(QwenChatConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,并指定实例级拦截器。
|
||||
* <p>
|
||||
* 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
|
||||
* 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
* @param userInterceptors 实例级拦截器列表
|
||||
*/
|
||||
public QwenChatModel(QwenChatConfig config, List<ChatInterceptor> userInterceptors) {
|
||||
super(config, userInterceptors);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ChatRequestSpecBuilder getChatRequestSpecBuilder() {
|
||||
return new QwenRequestSpecBuilder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.easyagents.llm.qwen;
|
||||
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <link href="https://help.aliyun.com/zh/model-studio/use-qwen-by-calling-api">通义千问API参考</link>
|
||||
*
|
||||
* @author liutf
|
||||
*/
|
||||
public class QwenChatOptions extends ChatOptions {
|
||||
/**
|
||||
* 输出数据的模态,仅支持 Qwen-Omni 模型指定。(可选)
|
||||
* 默认值为["text"]
|
||||
* 可选值:
|
||||
* ["text"]:输出文本。
|
||||
*/
|
||||
private List<String> modalities;
|
||||
|
||||
/**
|
||||
* 控制模型生成文本时的内容重复度。(可选)
|
||||
* 取值范围:[-2.0, 2.0]。正数会减少重复度,负数会增加重复度。
|
||||
* <pre>
|
||||
* 适用场景:
|
||||
* 较高的presence_penalty适用于要求多样性、趣味性或创造性的场景,如创意写作或头脑风暴。
|
||||
* 较低的presence_penalty适用于要求一致性或专业术语的场景,如技术文档或其他正式文档。
|
||||
* </pre>
|
||||
* 不建议修改QVQ模型的默认presence_penalty值。
|
||||
*/
|
||||
private Float presencePenalty;
|
||||
|
||||
/**
|
||||
* 生成响应的个数,取值范围是1-4。
|
||||
* 对于需要生成多个响应的场景(如创意写作、广告文案等),可以设置较大的 n 值。
|
||||
* <pre>
|
||||
* 当前仅支持 qwen-plus 模型,且在传入 tools 参数时固定为1。
|
||||
* 设置较大的 n 值不会增加输入 Token 消耗,会增加输出 Token 的消耗。
|
||||
* </pre>
|
||||
*/
|
||||
private Integer n;
|
||||
|
||||
/**
|
||||
* 是否开启并行工具调用。
|
||||
* 参数为true时开启,为false时不开启。
|
||||
* 并行工具调用请参见:https://help.aliyun.com/zh/model-studio/qwen-function-calling#cb6b5c484bt4x
|
||||
*/
|
||||
private Boolean parallelToolCalls;
|
||||
|
||||
/**
|
||||
* 当您使用翻译模型时需要配置的翻译参数。
|
||||
*/
|
||||
private TranslationOptions translationOptions;
|
||||
|
||||
/**
|
||||
* 用于控制模型在生成文本时是否使用互联网搜索结果进行参考。
|
||||
* 取值如下:
|
||||
* True:启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑判断是否使用互联网搜索结果。
|
||||
* False(默认):关闭互联网搜索。
|
||||
* 启用互联网搜索功能可能会增加 Token 的消耗。
|
||||
* 当前支持 qwen-max、qwen-plus、qwen-turbo
|
||||
*/
|
||||
private Boolean enableSearch;
|
||||
|
||||
|
||||
/**
|
||||
* 思考模式预算,适用于 Qwen3 模型。
|
||||
* 思考过程的最大长度,只在 enable_thinking 为 true 时生效。适用于 Qwen3 的商业版与开源版模型。
|
||||
* 详情请参见限制思考长度:https://help.aliyun.com/zh/model-studio/deep-thinking#e7c0002fe4meu
|
||||
*/
|
||||
private Integer thinkingBudget;
|
||||
|
||||
/**
|
||||
* 联网搜索的策略。
|
||||
* 仅当enable_search为true时生效。
|
||||
*/
|
||||
private SearchOptions searchOptions;
|
||||
|
||||
public static class TranslationOptions {
|
||||
/**
|
||||
* (必选)
|
||||
* 源语言的英文全称
|
||||
* 您可以将source_lang设置为"auto",模型会自动判断输入文本属于哪种语言。
|
||||
* 支持的语言: https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#038d2865bbydc
|
||||
*/
|
||||
@JSONField(name = "source_lang")
|
||||
private String sourceLang;
|
||||
|
||||
/**
|
||||
* (必选)
|
||||
* 目标语言的英文全称,
|
||||
* 支持的语言: https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#038d2865bbydc
|
||||
*/
|
||||
@JSONField(name = "target_lang")
|
||||
private String targetLang;
|
||||
|
||||
/**
|
||||
* 在使用术语干预翻译功能时需要设置的术语数组。
|
||||
* https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#2bf54a5ab5voe
|
||||
*/
|
||||
@JSONField(name = "terms")
|
||||
private List<TranslationOptionsExt> terms;
|
||||
|
||||
/**
|
||||
* 在使用翻译记忆功能时需要设置的翻译记忆数组。
|
||||
* https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#17e15234e7gfp
|
||||
*/
|
||||
@JSONField(name = "tm_list")
|
||||
private List<TranslationOptionsExt> tmList;
|
||||
|
||||
/**
|
||||
* (可选)
|
||||
* 在使用领域提示功能时需要设置的领域提示语句。
|
||||
* 领域提示语句暂时只支持英文。
|
||||
* https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#4af23a31db7lf
|
||||
*/
|
||||
@JSONField(name = "domains")
|
||||
private String domains;
|
||||
|
||||
public String getSourceLang() {
|
||||
return sourceLang;
|
||||
}
|
||||
|
||||
public TranslationOptions setSourceLang(String sourceLang) {
|
||||
this.sourceLang = sourceLang;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTargetLang() {
|
||||
return targetLang;
|
||||
}
|
||||
|
||||
public TranslationOptions setTargetLang(String targetLang) {
|
||||
this.targetLang = targetLang;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<TranslationOptionsExt> getTerms() {
|
||||
return terms;
|
||||
}
|
||||
|
||||
public TranslationOptions setTerms(List<TranslationOptionsExt> terms) {
|
||||
this.terms = terms;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<TranslationOptionsExt> getTmList() {
|
||||
return tmList;
|
||||
}
|
||||
|
||||
public TranslationOptions setTmList(List<TranslationOptionsExt> tmList) {
|
||||
this.tmList = tmList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDomains() {
|
||||
return domains;
|
||||
}
|
||||
|
||||
public TranslationOptions setDomains(String domains) {
|
||||
this.domains = domains;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TranslationOptionsExt {
|
||||
/**
|
||||
* 源语言的术语/源语言的语句
|
||||
*/
|
||||
private String source;
|
||||
/**
|
||||
* 目标语言的术语/目标语言的语句
|
||||
*/
|
||||
private String target;
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public TranslationOptionsExt setSource(String source) {
|
||||
this.source = source;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public TranslationOptionsExt setTarget(String target) {
|
||||
this.target = target;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class SearchOptions {
|
||||
/**
|
||||
* 是否强制开启搜索。(可选)默认值为false
|
||||
* 参数值:true=强制开启;false=不强制开启。
|
||||
*/
|
||||
@JSONField(name = "forced_search")
|
||||
private Boolean forcedSearch;
|
||||
|
||||
/**
|
||||
* 搜索互联网信息的数量,(可选)默认值为"standard"
|
||||
* 参数值:
|
||||
* standard:在请求时搜索5条互联网信息;
|
||||
* pro:在请求时搜索10条互联网信息。
|
||||
*/
|
||||
@JSONField(name = "search_strategy")
|
||||
private String searchStrategy;
|
||||
|
||||
public Boolean getForcedSearch() {
|
||||
return forcedSearch;
|
||||
}
|
||||
|
||||
public SearchOptions setForcedSearch(Boolean forcedSearch) {
|
||||
this.forcedSearch = forcedSearch;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSearchStrategy() {
|
||||
return searchStrategy;
|
||||
}
|
||||
|
||||
public SearchOptions setSearchStrategy(String searchStrategy) {
|
||||
this.searchStrategy = searchStrategy;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getModalities() {
|
||||
return modalities;
|
||||
}
|
||||
|
||||
public QwenChatOptions setModalities(List<String> modalities) {
|
||||
this.modalities = modalities;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Float getPresencePenalty() {
|
||||
return presencePenalty;
|
||||
}
|
||||
|
||||
public QwenChatOptions setPresencePenalty(Float presencePenalty) {
|
||||
this.presencePenalty = presencePenalty;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getN() {
|
||||
return n;
|
||||
}
|
||||
|
||||
public QwenChatOptions setN(Integer n) {
|
||||
this.n = n;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Boolean getParallelToolCalls() {
|
||||
return parallelToolCalls;
|
||||
}
|
||||
|
||||
public QwenChatOptions setParallelToolCalls(Boolean parallelToolCalls) {
|
||||
this.parallelToolCalls = parallelToolCalls;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranslationOptions getTranslationOptions() {
|
||||
return translationOptions;
|
||||
}
|
||||
|
||||
public QwenChatOptions setTranslationOptions(TranslationOptions translationOptions) {
|
||||
this.translationOptions = translationOptions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Boolean getEnableSearch() {
|
||||
return enableSearch;
|
||||
}
|
||||
|
||||
public QwenChatOptions setEnableSearch(Boolean enableSearch) {
|
||||
this.enableSearch = enableSearch;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getThinkingBudget() {
|
||||
return thinkingBudget;
|
||||
}
|
||||
|
||||
public void setThinkingBudget(Integer thinkingBudget) {
|
||||
this.thinkingBudget = thinkingBudget;
|
||||
}
|
||||
|
||||
public SearchOptions getSearchOptions() {
|
||||
return searchOptions;
|
||||
}
|
||||
|
||||
public QwenChatOptions setSearchOptions(SearchOptions searchOptions) {
|
||||
this.searchOptions = searchOptions;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.easyagents.llm.qwen;
|
||||
|
||||
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.CollectionUtil;
|
||||
import com.easyagents.core.util.Maps;
|
||||
|
||||
public class QwenRequestSpecBuilder extends OpenAIChatRequestSpecBuilder {
|
||||
|
||||
@Override
|
||||
protected Maps buildBaseParamsOfRequestBody(Prompt prompt, ChatOptions options, ChatConfig config) {
|
||||
Maps params = super.buildBaseParamsOfRequestBody(prompt, options, config);
|
||||
if (options instanceof QwenChatOptions) {
|
||||
QwenChatOptions op = (QwenChatOptions) options;
|
||||
params.setIf(CollectionUtil.hasItems(op.getModalities()), "modalities", op.getModalities());
|
||||
params.setIf(op.getPresencePenalty() != null, "presence_penalty", op.getPresencePenalty());
|
||||
params.setIf(op.getResponseFormat() != null, "response_format", op.getResponseFormat());
|
||||
params.setIf(op.getN() != null, "n", op.getN());
|
||||
params.setIf(op.getParallelToolCalls() != null, "parallel_tool_calls", op.getParallelToolCalls());
|
||||
params.setIf(op.getTranslationOptions() != null, "translation_options", op.getTranslationOptions());
|
||||
params.setIf(op.getEnableSearch() != null, "enable_search", op.getEnableSearch());
|
||||
params.setIf(op.getEnableSearch() != null && op.getEnableSearch() && op.getSearchOptions() != null, "search_options", op.getSearchOptions());
|
||||
params.setIf(op.getThinkingEnabled() != null, "enable_thinking", op.getThinkingEnabled());
|
||||
params.setIf(op.getThinkingEnabled() != null && op.getThinkingEnabled() && op.getThinkingBudget() != null, "thinking_budget", op.getThinkingBudget());
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.easyagents.llm.qwen.test;
|
||||
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.easyagents.core.model.chat.response.AiMessageResponse;
|
||||
import com.easyagents.core.model.exception.ModelException;
|
||||
import com.easyagents.core.prompt.SimplePrompt;
|
||||
|
||||
import com.easyagents.llm.qwen.QwenChatConfig;
|
||||
import com.easyagents.llm.qwen.QwenChatModel;
|
||||
import com.easyagents.llm.qwen.QwenChatOptions;
|
||||
import com.easyagents.llm.qwen.QwenChatOptions.SearchOptions;
|
||||
import org.junit.Test;
|
||||
|
||||
public class QwenTest {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
QwenChatConfig config = new QwenChatConfig();
|
||||
|
||||
//https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
||||
config.setApiKey("sk-28a6be3236****");
|
||||
config.setModel("qwen-plus");
|
||||
|
||||
ChatModel chatModel = new QwenChatModel(config);
|
||||
chatModel.chatStream("请写一个小兔子战胜大灰狼的故事", (context, response) -> {
|
||||
AiMessage message = response.getMessage();
|
||||
System.out.println(">>>> " + message.getContent());
|
||||
});
|
||||
|
||||
Thread.sleep(10000);
|
||||
}
|
||||
|
||||
@Test(expected = ModelException.class)
|
||||
public void testForcedSearch() throws InterruptedException {
|
||||
QwenChatConfig config = new QwenChatConfig();
|
||||
config.setApiKey("sk-28a6be3236****");
|
||||
config.setModel("qwen-max");
|
||||
|
||||
ChatModel chatModel = new QwenChatModel(config);
|
||||
QwenChatOptions options = new QwenChatOptions();
|
||||
options.setEnableSearch(true);
|
||||
options.setSearchOptions(new SearchOptions().setForcedSearch(true));
|
||||
|
||||
String responseStr = chatModel.chat("今天是几号?", options);
|
||||
|
||||
System.out.println(responseStr);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionCalling() throws InterruptedException {
|
||||
QwenChatConfig config = new QwenChatConfig();
|
||||
config.setApiKey("sk-28a6be3236****");
|
||||
config.setModel("qwen-turbo");
|
||||
|
||||
ChatModel chatModel = new QwenChatModel(config);
|
||||
|
||||
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
|
||||
prompt.addToolsFromClass(WeatherFunctions.class);
|
||||
AiMessageResponse response = chatModel.chat(prompt);
|
||||
|
||||
System.out.println(response.executeToolCallsAndGetResults());
|
||||
// "Today it will be dull and overcast in 北京"
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态替换模型
|
||||
*/
|
||||
@Test
|
||||
public void testDynamicModel() throws InterruptedException {
|
||||
// 默认模型
|
||||
QwenChatConfig config = new QwenChatConfig();
|
||||
config.setApiKey("sk-28a6be3236****");
|
||||
config.setModel("qwen-turbo");
|
||||
|
||||
// 运行时动态替换模型
|
||||
ChatOptions options = new QwenChatOptions();
|
||||
options.setModel("deepseek-r1");
|
||||
|
||||
ChatModel chatModel = new QwenChatModel(config);
|
||||
chatModel.chatStream("请写一个小兔子战胜大灰狼的故事", (context, response) -> {
|
||||
AiMessage message = response.getMessage();
|
||||
System.err.println(message.getReasoningContent());
|
||||
System.out.println(message.getFullContent());
|
||||
System.out.println();
|
||||
}, options);
|
||||
Thread.sleep(10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试千问3 开启思考模式的开关
|
||||
*/
|
||||
@Test
|
||||
public void testQwen3Thinking() throws InterruptedException {
|
||||
QwenChatConfig config = new QwenChatConfig();
|
||||
config.setApiKey("sk-28a6be3236****");
|
||||
config.setModel("qwen3-235b-a22b");
|
||||
|
||||
ChatModel chatModel = new QwenChatModel(config);
|
||||
QwenChatOptions options = new QwenChatOptions();
|
||||
options.setThinkingEnabled(false);
|
||||
//options.setThinkingBudget(1024);
|
||||
|
||||
chatModel.chatStream("你是谁", (context, response) -> {
|
||||
AiMessage message = response.getMessage();
|
||||
System.err.println(message.getReasoningContent());
|
||||
System.out.println(message.getFullContent());
|
||||
System.out.println();
|
||||
}, options);
|
||||
Thread.sleep(10000);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.easyagents.llm.qwen.test;
|
||||
|
||||
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 "Today it will be dull and overcast in " + name;
|
||||
}
|
||||
}
|
||||
29
easy-agents-chat/pom.xml
Normal file
29
easy-agents-chat/pom.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-chat</name>
|
||||
<artifactId>easy-agents-chat</artifactId>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>easy-agents-chat-openai</module>
|
||||
<module>easy-agents-chat-qwen</module>
|
||||
<module>easy-agents-chat-ollama</module>
|
||||
<module>easy-agents-chat-deepseek</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
120
easy-agents-core/pom.xml
Normal file
120
easy-agents-core/pom.xml
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-core</name>
|
||||
<artifactId>easy-agents-core</artifactId>
|
||||
|
||||
<properties>
|
||||
<poi.version>5.5.1</poi.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp-sse</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.knuddels</groupId>
|
||||
<artifactId>jtokkit</artifactId>
|
||||
<version>1.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-scratchpad</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>2.0.30</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.18.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.11.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- OpenTelemetry API -->
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenTelemetry SDK(运行时) -->
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-sdk</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 导出到控制台(轻量级,无需第三方) -->
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-exporter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-exporter-otlp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.agent.IAgent;
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.message.ToolCall;
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.easyagents.core.model.chat.StreamResponseListener;
|
||||
import com.easyagents.core.model.chat.response.AiMessageResponse;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.core.model.chat.tool.ToolExecutor;
|
||||
import com.easyagents.core.model.chat.tool.ToolInterceptor;
|
||||
import com.easyagents.core.model.client.StreamContext;
|
||||
import com.easyagents.core.prompt.MemoryPrompt;
|
||||
import com.easyagents.core.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ReActAgent 是一个通用的 ReAct 模式 Agent,支持 Reasoning + Action 的交互方式。
|
||||
*/
|
||||
public class ReActAgent implements IAgent {
|
||||
|
||||
public static final String PARENT_AGENT_KEY = "__parent_react_agent";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ReActAgent.class);
|
||||
|
||||
private static final String DEFAULT_PROMPT_TEMPLATE =
|
||||
"你是一个 ReAct Agent,结合 Reasoning(推理)和 Action(行动)来解决问题。\n" +
|
||||
"但在处理用户问题时,请首先判断:\n" +
|
||||
"1. 如果问题可以通过你的常识或已有知识直接回答 → 请忽略 ReAct 框架,直接输出自然语言回答。\n" +
|
||||
"2. 如果问题需要调用特定工具才能解决(如查询、计算、获取外部信息等)→ 请严格按照 ReAct 格式响应。\n" +
|
||||
"\n" +
|
||||
"如果你选择使用 ReAct 模式,请遵循以下格式:\n" +
|
||||
"Thought: 描述你对当前问题的理解,包括已知信息和缺失信息,说明你下一步将采取什么行动及其原因。\n" +
|
||||
"Action: 从下方列出的工具中选择一个合适的工具,仅输出工具名称,不得虚构。\n" +
|
||||
"Action Input: 使用标准 JSON 格式提供该工具所需的参数,确保字段名与工具描述一致。\n" +
|
||||
"\n" +
|
||||
"在 ReAct 模式下,如果你已获得足够信息可以直接回答用户,请输出:\n" +
|
||||
"Final Answer: [你的回答]\n" +
|
||||
"\n" +
|
||||
"如果你发现用户的问题缺少关键信息(例如时间、地点、具体目标、主体信息等),且无法通过工具获取,\n" +
|
||||
"请主动向用户提问,格式如下:\n" +
|
||||
"Request: [你希望用户澄清的问题]" +
|
||||
"\n" +
|
||||
"注意事项:\n" +
|
||||
"1. 每次只能选择一个工具并执行一个动作。\n" +
|
||||
"2. 在未收到工具执行结果前,不要自行假设其输出。\n" +
|
||||
"3. 不得编造工具或参数,所有工具均列于下方。\n" +
|
||||
"4. 输出顺序必须为:Thought → Action → Action Input。\n" +
|
||||
"\n" +
|
||||
"### 可用工具列表:\n" +
|
||||
"{tools}\n" +
|
||||
"\n" +
|
||||
"### 用户问题如下:\n" +
|
||||
"{user_input}";
|
||||
|
||||
private static final int DEFAULT_MAX_ITERATIONS = 20;
|
||||
|
||||
private final ChatModel chatModel;
|
||||
private final List<Tool> tools;
|
||||
private final ReActAgentState state;
|
||||
|
||||
private ReActStepParser reActStepParser = ReActStepParser.DEFAULT; // 默认解析器
|
||||
private final MemoryPrompt memoryPrompt;
|
||||
private ChatOptions chatOptions;
|
||||
private ReActMessageBuilder messageBuilder = new ReActMessageBuilder();
|
||||
|
||||
// 监听器集合
|
||||
private final List<ReActAgentListener> listeners = new ArrayList<>();
|
||||
|
||||
// 拦截器集合
|
||||
private final List<ToolInterceptor> toolInterceptors = new ArrayList<>();
|
||||
|
||||
|
||||
public ReActAgent(ChatModel chatModel, List<Tool> tools, String userQuery) {
|
||||
this.chatModel = chatModel;
|
||||
this.tools = tools;
|
||||
this.state = new ReActAgentState();
|
||||
this.state.userQuery = userQuery;
|
||||
this.state.promptTemplate = DEFAULT_PROMPT_TEMPLATE;
|
||||
this.state.maxIterations = DEFAULT_MAX_ITERATIONS;
|
||||
this.memoryPrompt = new MemoryPrompt();
|
||||
}
|
||||
|
||||
public ReActAgent(ChatModel chatModel, List<Tool> tools, String userQuery, MemoryPrompt memoryPrompt) {
|
||||
this.chatModel = chatModel;
|
||||
this.tools = tools;
|
||||
this.state = new ReActAgentState();
|
||||
this.state.userQuery = userQuery;
|
||||
this.state.promptTemplate = DEFAULT_PROMPT_TEMPLATE;
|
||||
this.state.maxIterations = DEFAULT_MAX_ITERATIONS;
|
||||
this.memoryPrompt = memoryPrompt;
|
||||
}
|
||||
|
||||
public ReActAgent(ChatModel chatModel, List<Tool> tools, ReActAgentState state) {
|
||||
this.chatModel = chatModel;
|
||||
this.tools = tools;
|
||||
this.state = state;
|
||||
this.memoryPrompt = new MemoryPrompt();
|
||||
if (state.messageHistory != null) {
|
||||
this.memoryPrompt.addMessages(state.messageHistory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册监听器
|
||||
*/
|
||||
public void addListener(ReActAgentListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除监听器
|
||||
*/
|
||||
public void removeListener(ReActAgentListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void addToolInterceptor(ToolInterceptor interceptor) {
|
||||
toolInterceptors.add(interceptor);
|
||||
}
|
||||
|
||||
public List<ToolInterceptor> getToolInterceptors() {
|
||||
return toolInterceptors;
|
||||
}
|
||||
|
||||
public ChatModel getChatModel() {
|
||||
return chatModel;
|
||||
}
|
||||
|
||||
public List<Tool> getTools() {
|
||||
return tools;
|
||||
}
|
||||
|
||||
public ReActStepParser getReActStepParser() {
|
||||
return reActStepParser;
|
||||
}
|
||||
|
||||
public void setReActStepParser(ReActStepParser reActStepParser) {
|
||||
this.reActStepParser = reActStepParser;
|
||||
}
|
||||
|
||||
public List<ReActAgentListener> getListeners() {
|
||||
return listeners;
|
||||
}
|
||||
|
||||
public boolean isStreamable() {
|
||||
return this.state.streamable;
|
||||
}
|
||||
|
||||
public void setStreamable(boolean streamable) {
|
||||
this.state.streamable = streamable;
|
||||
}
|
||||
|
||||
|
||||
public MemoryPrompt getMemoryPrompt() {
|
||||
return memoryPrompt;
|
||||
}
|
||||
|
||||
public ReActMessageBuilder getMessageBuilder() {
|
||||
return messageBuilder;
|
||||
}
|
||||
|
||||
public void setMessageBuilder(ReActMessageBuilder messageBuilder) {
|
||||
this.messageBuilder = messageBuilder;
|
||||
}
|
||||
|
||||
public ChatOptions getChatOptions() {
|
||||
return chatOptions;
|
||||
}
|
||||
|
||||
public void setChatOptions(ChatOptions chatOptions) {
|
||||
this.chatOptions = chatOptions;
|
||||
}
|
||||
|
||||
public ReActAgentState getState() {
|
||||
state.messageHistory = memoryPrompt.getMessages();
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 ReAct Agent 流程
|
||||
*/
|
||||
@Override
|
||||
public void execute() {
|
||||
try {
|
||||
List<Message> messageHistory = state.getMessageHistory();
|
||||
if (messageHistory == null || messageHistory.isEmpty()) {
|
||||
String toolsDescription = Util.buildToolsDescription(tools);
|
||||
String prompt = state.promptTemplate
|
||||
.replace("{tools}", toolsDescription)
|
||||
.replace("{user_input}", state.userQuery);
|
||||
|
||||
Message message = messageBuilder.buildStartMessage(prompt, tools, state.userQuery);
|
||||
memoryPrompt.addMessage(message);
|
||||
}
|
||||
if (this.isStreamable()) {
|
||||
startNextReActStepStream();
|
||||
} else {
|
||||
startNextReactStepNormal();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("运行 ReAct Agent 出错:" + e);
|
||||
notifyOnError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void startNextReactStepNormal() {
|
||||
while (state.iterationCount < state.maxIterations) {
|
||||
|
||||
state.iterationCount++;
|
||||
|
||||
AiMessageResponse response = chatModel.chat(memoryPrompt, chatOptions);
|
||||
notifyOnChatResponse(response);
|
||||
|
||||
String content = response.getMessage().getContent();
|
||||
AiMessage message = new AiMessage(content);
|
||||
|
||||
// 请求用户输入
|
||||
if (isRequestUserInput(content)) {
|
||||
String question = extractRequestQuestion(content);
|
||||
message.addMetadata("type", "reActRequest");
|
||||
memoryPrompt.addMessage(message);
|
||||
notifyOnRequestUserInput(question); // 新增监听器回调
|
||||
break; // 暂停执行,等待用户回复
|
||||
}
|
||||
// ReAct 动作
|
||||
else if (isReActAction(content)) {
|
||||
message.addMetadata("type", "reActAction");
|
||||
memoryPrompt.addMessage(message);
|
||||
if (!processReActSteps(content)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 最终答案
|
||||
else if (isFinalAnswer(content)) {
|
||||
String flag = reActStepParser.getFinalAnswerFlag();
|
||||
String answer = content.substring(content.indexOf(flag) + flag.length());
|
||||
message.addMetadata("type", "reActFinalAnswer");
|
||||
memoryPrompt.addMessage(message);
|
||||
notifyOnFinalAnswer(answer);
|
||||
break;
|
||||
}
|
||||
// 不是 Action
|
||||
else {
|
||||
memoryPrompt.addMessage(message);
|
||||
notifyOnNonActionResponse(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 显式通知达到最大迭代
|
||||
if (state.iterationCount >= state.maxIterations) {
|
||||
notifyOnMaxIterationsReached();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void startNextReActStepStream() {
|
||||
if (state.iterationCount >= state.maxIterations) {
|
||||
notifyOnMaxIterationsReached();
|
||||
return;
|
||||
}
|
||||
|
||||
state.iterationCount++;
|
||||
|
||||
chatModel.chatStream(memoryPrompt, new StreamResponseListener() {
|
||||
|
||||
@Override
|
||||
public void onMessage(StreamContext context, AiMessageResponse response) {
|
||||
notifyOnChatResponseStream(context, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(StreamContext context) {
|
||||
AiMessage lastAiMessage = context.getFullMessage();
|
||||
if (lastAiMessage == null) {
|
||||
notifyOnError(new RuntimeException("没有收到任何回复"));
|
||||
return;
|
||||
}
|
||||
|
||||
String content = lastAiMessage.getFullContent();
|
||||
if (StringUtil.noText(content)) {
|
||||
notifyOnError(new RuntimeException("没有收到任何回复"));
|
||||
return;
|
||||
}
|
||||
|
||||
AiMessage message = new AiMessage(content);
|
||||
|
||||
// 请求用户输入
|
||||
if (isRequestUserInput(content)) {
|
||||
String question = extractRequestQuestion(content);
|
||||
message.addMetadata("type", "reActRequest");
|
||||
memoryPrompt.addMessage(message);
|
||||
notifyOnRequestUserInput(question); // 新增监听器回调
|
||||
}
|
||||
|
||||
// ReAct 动作
|
||||
else if (isReActAction(content)) {
|
||||
message.addMetadata("type", "reActAction");
|
||||
memoryPrompt.addMessage(message);
|
||||
if (processReActSteps(content)) {
|
||||
// 递归继续执行下一个 ReAct 步骤
|
||||
startNextReActStepStream();
|
||||
}
|
||||
}
|
||||
|
||||
// 最终答案
|
||||
else if (isFinalAnswer(content)) {
|
||||
message.addMetadata("type", "reActFinalAnswer");
|
||||
memoryPrompt.addMessage(message);
|
||||
String flag = reActStepParser.getFinalAnswerFlag();
|
||||
String answer = content.substring(content.indexOf(flag) + flag.length());
|
||||
notifyOnFinalAnswer(answer);
|
||||
} else {
|
||||
memoryPrompt.addMessage(message);
|
||||
// 不是 Action
|
||||
notifyOnNonActionResponseStream(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(StreamContext context, Throwable throwable) {
|
||||
notifyOnError((Exception) throwable);
|
||||
}
|
||||
}, chatOptions);
|
||||
}
|
||||
|
||||
|
||||
private boolean isFinalAnswer(String content) {
|
||||
return reActStepParser.isFinalAnswer(content);
|
||||
}
|
||||
|
||||
private boolean isReActAction(String content) {
|
||||
return reActStepParser.isReActAction(content);
|
||||
}
|
||||
|
||||
private boolean isRequestUserInput(String content) {
|
||||
return reActStepParser.isRequest(content);
|
||||
}
|
||||
|
||||
private String extractRequestQuestion(String content) {
|
||||
return reActStepParser.extractRequestQuestion(content);
|
||||
}
|
||||
|
||||
|
||||
private boolean processReActSteps(String content) {
|
||||
List<ReActStep> reActSteps = reActStepParser.parse(content);
|
||||
if (reActSteps.isEmpty()) {
|
||||
notifyOnStepParseError(content);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ReActStep step : reActSteps) {
|
||||
boolean stepExecuted = false;
|
||||
for (Tool tool : tools) {
|
||||
if (tool.getName().equals(step.getAction())) {
|
||||
try {
|
||||
notifyOnActionStart(step);
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
ToolCall toolCall = new ToolCall();
|
||||
toolCall.setId("react_call_" + state.iterationCount + "_" + System.currentTimeMillis());
|
||||
toolCall.setName(step.getAction());
|
||||
toolCall.setArguments(step.getActionInput());
|
||||
|
||||
ToolExecutor executor = new ToolExecutor(tool, toolCall, toolInterceptors);
|
||||
|
||||
// 方便 “子Agent” 或者 tool, 获取当前的 ReActAgent
|
||||
executor.addInterceptor((context, chain) -> {
|
||||
context.setAttribute(PARENT_AGENT_KEY, ReActAgent.this);
|
||||
return chain.proceed(context);
|
||||
});
|
||||
|
||||
result = executor.execute();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
notifyOnActionEnd(step, result);
|
||||
}
|
||||
|
||||
Message message = messageBuilder.buildObservationMessage(step, result);
|
||||
memoryPrompt.addMessage(message);
|
||||
stepExecuted = true;
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
notifyOnActionInvokeError(e);
|
||||
|
||||
if (!state.continueOnActionInvokeError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Message message = messageBuilder.buildActionErrorMessage(step, e);
|
||||
memoryPrompt.addMessage(message);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stepExecuted) {
|
||||
notifyOnActionNotMatched(step, tools);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ========== 通知监听器的方法 ==========
|
||||
private void notifyOnChatResponse(AiMessageResponse response) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onChatResponse(response);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnNonActionResponse(AiMessageResponse response) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onNonActionResponse(response);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnNonActionResponseStream(StreamContext context) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onNonActionResponseStream(context);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnChatResponseStream(StreamContext context, AiMessageResponse response) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onChatResponseStream(context, response);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnFinalAnswer(String finalAnswer) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onFinalAnswer(finalAnswer);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnRequestUserInput(String question) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onRequestUserInput(question);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnActionStart(ReActStep reActStep) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onActionStart(reActStep);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnActionEnd(ReActStep reActStep, Object result) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onActionEnd(reActStep, result);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnMaxIterationsReached() {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onMaxIterationsReached();
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnStepParseError(String content) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onStepParseError(content);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnActionNotMatched(ReActStep step, List<Tool> tools) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onActionNotMatched(step, tools);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnActionInvokeError(Exception e) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onActionInvokeError(e);
|
||||
} catch (Exception e1) {
|
||||
log.error(e.toString(), e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnError(Exception e) {
|
||||
for (ReActAgentListener listener : listeners) {
|
||||
try {
|
||||
listener.onError(e);
|
||||
} catch (Exception e1) {
|
||||
log.error(e.toString(), e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.client.StreamContext;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.core.model.chat.response.AiMessageResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ReActAgent 的监听器接口,用于监听执行过程中的关键事件。
|
||||
*/
|
||||
public interface ReActAgentListener {
|
||||
|
||||
/**
|
||||
* 当 LLM 生成响应时触发
|
||||
*
|
||||
* @param response 原始响应内容
|
||||
*/
|
||||
default void onChatResponse(AiMessageResponse response) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当 LLM 生成响应时触发
|
||||
*
|
||||
* @param context 上下文信息
|
||||
* @param response 原始响应内容
|
||||
*/
|
||||
default void onChatResponseStream(StreamContext context, AiMessageResponse response) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 当未命中工具时触发
|
||||
*
|
||||
* @param response 原始响应内容
|
||||
*/
|
||||
default void onNonActionResponse(AiMessageResponse response) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当未命中工具时触发
|
||||
*/
|
||||
default void onNonActionResponseStream(StreamContext context) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 当检测到最终答案时触发
|
||||
*
|
||||
* @param finalAnswer 最终答案内容
|
||||
*/
|
||||
default void onFinalAnswer(String finalAnswer) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 当需要用户输入时触发
|
||||
*/
|
||||
default void onRequestUserInput(String question) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当调用工具前触发
|
||||
*
|
||||
* @param step 当前步骤
|
||||
*/
|
||||
default void onActionStart(ReActStep step) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当调用工具完成后触发
|
||||
*
|
||||
* @param step 工具名称
|
||||
* @param result 工具返回结果
|
||||
*/
|
||||
default void onActionEnd(ReActStep step, Object result) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当达到最大迭代次数仍未获得答案时触发
|
||||
*/
|
||||
default void onMaxIterationsReached() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 当解析步骤时发生错误时触发
|
||||
*
|
||||
* @param content 错误内容
|
||||
*/
|
||||
default void onStepParseError(String content) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当未匹配到任何工具时触发
|
||||
*
|
||||
* @param step 当前步骤
|
||||
* @param tools 可用的工具列表
|
||||
*/
|
||||
default void onActionNotMatched(ReActStep step, List<Tool> tools) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当工具执行错误时触发
|
||||
*
|
||||
* @param e 错误对象
|
||||
*/
|
||||
default void onActionInvokeError(Exception e) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当工具返回的 JSON 格式错误时触发
|
||||
*
|
||||
* @param step 当前步骤
|
||||
* @param error 错误对象
|
||||
*/
|
||||
default void onActionJsonParserError(ReActStep step, Exception error) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当发生异常时触发
|
||||
*
|
||||
* @param error 异常对象
|
||||
*/
|
||||
default void onError(Exception error) {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Message> 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<Message> getMessageHistory() {
|
||||
return messageHistory;
|
||||
}
|
||||
|
||||
public void setMessageHistory(List<Message> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
///*
|
||||
// * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
// * <p>
|
||||
// * 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
|
||||
// * <p>
|
||||
// * http://www.apache.org/licenses/LICENSE-2.0
|
||||
// * <p>
|
||||
// * 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<String, Object> 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;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Tool> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public interface ReActStepParser {
|
||||
|
||||
ReActStepParser DEFAULT = content -> {
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<ReActStep> steps = new ArrayList<>();
|
||||
String[] lines = content.split("\n");
|
||||
|
||||
String currentThought = null;
|
||||
String currentAction = null;
|
||||
String currentRequest = null;
|
||||
StringBuilder currentActionInput = new StringBuilder();
|
||||
boolean inActionInput = false;
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
|
||||
// 如果遇到新的 Thought,且已有完整步骤,先保存
|
||||
if (trimmedLine.startsWith("Thought:")) {
|
||||
if (currentThought != null && currentAction != null) {
|
||||
// 保存上一个完整的 step(在遇到新 Thought 或 Final Answer 时触发)
|
||||
steps.add(new ReActStep(currentThought, currentAction, currentActionInput.toString().trim()));
|
||||
// 重置状态
|
||||
currentActionInput.setLength(0);
|
||||
inActionInput = false;
|
||||
}
|
||||
currentThought = trimmedLine.substring("Thought:".length()).trim();
|
||||
currentAction = null;
|
||||
}
|
||||
// 如果遇到 Action
|
||||
else if (trimmedLine.startsWith("Action:")) {
|
||||
if (currentThought == null) {
|
||||
// 如果 Action 出现在 Thought 之前,视为格式错误,可选择忽略或报错
|
||||
continue;
|
||||
}
|
||||
currentAction = trimmedLine.substring("Action:".length()).trim();
|
||||
}
|
||||
// 如果遇到 Action Input
|
||||
else if (trimmedLine.startsWith("Action Input:")) {
|
||||
if (currentAction == null) {
|
||||
// Action Input 出现在 Action 之前,跳过
|
||||
continue;
|
||||
}
|
||||
String inputPart = trimmedLine.substring("Action Input:".length()).trim();
|
||||
currentActionInput.append(inputPart);
|
||||
inActionInput = true;
|
||||
}
|
||||
// 如果正在读取 Action Input 的后续行(多行 JSON)
|
||||
else if (inActionInput) {
|
||||
// 判断是否是下一个结构的开始:Thought / Action / Final Answer
|
||||
if (trimmedLine.startsWith("Thought:") ||
|
||||
trimmedLine.startsWith("Action:") ||
|
||||
trimmedLine.startsWith("Final Answer:")) {
|
||||
// 实际上这一行属于下一段,应退回处理
|
||||
// 但我们是在 for 循环里,无法“退回”,所以先保存当前 step
|
||||
steps.add(new ReActStep(currentThought, currentAction, currentActionInput.toString().trim()));
|
||||
currentActionInput.setLength(0);
|
||||
inActionInput = false;
|
||||
currentThought = null;
|
||||
currentAction = null;
|
||||
// 重新处理当前行(递归或标记),但为简化,我们直接继续下一轮
|
||||
// 因为下一轮会处理 Thought/Action
|
||||
continue;
|
||||
} else {
|
||||
// 是 Action Input 的续行,追加(保留原始换行或加空格)
|
||||
if (currentActionInput.length() > 0) {
|
||||
currentActionInput.append("\n");
|
||||
}
|
||||
currentActionInput.append(line); // 保留原始缩进(可选)
|
||||
}
|
||||
}
|
||||
// 如果遇到 Final Answer,结束当前步骤(如果有)
|
||||
else if (trimmedLine.startsWith("Final Answer:")) {
|
||||
if (currentThought != null && currentAction != null) {
|
||||
steps.add(new ReActStep(currentThought, currentAction, currentActionInput.toString().trim()));
|
||||
}
|
||||
// Final Answer 本身不作为 ReActStep,通常单独处理
|
||||
break; // 或 continue,视需求而定
|
||||
}
|
||||
// 空行或无关行:如果是 Action Input 多行内容,已在上面处理;否则忽略
|
||||
}
|
||||
|
||||
// 循环结束后,检查是否还有未保存的步骤
|
||||
if (currentThought != null && currentAction != null) {
|
||||
steps.add(new ReActStep(currentThought, currentAction, currentActionInput.toString().trim()));
|
||||
}
|
||||
|
||||
return steps;
|
||||
};
|
||||
|
||||
|
||||
List<ReActStep> parse(String content);
|
||||
|
||||
|
||||
default boolean isFinalAnswer(String content) {
|
||||
return content.contains(getFinalAnswerFlag());
|
||||
}
|
||||
|
||||
default boolean isReActAction(String content) {
|
||||
return content.contains("Action:") && content.contains("Action Input:");
|
||||
}
|
||||
|
||||
default boolean isRequest(String content) {
|
||||
return content.contains("Request:");
|
||||
}
|
||||
|
||||
default String extractRequestQuestion(String content) {
|
||||
return content.trim().substring("Request:".length()).trim();
|
||||
}
|
||||
|
||||
default String getFinalAnswerFlag() {
|
||||
return "Final Answer:";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Tool> 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<Parameter> 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<Parameter> 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 等
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.model.chat.ChatModel;
|
||||
import com.easyagents.core.model.chat.ChatOptions;
|
||||
import com.easyagents.core.prompt.MemoryPrompt;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RouteAgent:负责路由用户输入到最合适的 IAgent。
|
||||
* - 不实现 IAgent 接口
|
||||
* - route() 方法返回 IAgent 实例,或 null(表示无匹配)
|
||||
* - 不处理 Direct Answer,一律返回 null
|
||||
* - 支持关键字快速匹配 + LLM 智能路由
|
||||
*/
|
||||
public class RoutingAgent {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RoutingAgent.class);
|
||||
|
||||
private static final String DEFAULT_ROUTING_PROMPT_TEMPLATE =
|
||||
"你是一个智能路由助手,请严格按以下规则响应:\n" +
|
||||
"\n" +
|
||||
"可用处理模块(Agent)及其能力描述:\n" +
|
||||
"{agent_descriptions}\n" +
|
||||
"\n" +
|
||||
"规则:\n" +
|
||||
"1. 如果用户问题属于某个模块的能力范围,请输出:Route: [模块名]\n" +
|
||||
"2. 如果问题可以直接回答(如问候、常识、简单对话),请输出:Direct: [你的自然语言回答]\n" +
|
||||
"3. 如果问题涉及多个模块,选择最核心的一个。\n" +
|
||||
"4. 不要解释、不要输出其他内容,只输出上述两种格式之一。\n" +
|
||||
"\n" +
|
||||
"当前对话上下文(最近几轮):\n" +
|
||||
"{conversation_context}\n" +
|
||||
"\n" +
|
||||
"用户最新问题:\n" +
|
||||
"{user_input}";
|
||||
|
||||
private final ChatModel chatModel;
|
||||
private final RoutingAgentRegistry routingAgentRegistry;
|
||||
private final String userQuery;
|
||||
private final MemoryPrompt memoryPrompt;
|
||||
|
||||
private String routingPromptTemplate = DEFAULT_ROUTING_PROMPT_TEMPLATE;
|
||||
private ChatOptions chatOptions;
|
||||
private boolean enableKeywordRouting = true;
|
||||
private boolean enableLlmRouting = true;
|
||||
|
||||
public RoutingAgent(ChatModel chatModel, RoutingAgentRegistry routingAgentRegistry,
|
||||
String userQuery, MemoryPrompt memoryPrompt) {
|
||||
this.chatModel = chatModel;
|
||||
this.routingAgentRegistry = routingAgentRegistry;
|
||||
this.userQuery = userQuery;
|
||||
this.memoryPrompt = memoryPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由用户输入,返回匹配的 IAgent 实例。
|
||||
* 仅当明确路由到某个 Agent 时才返回 IAgent,否则返回 null。
|
||||
*
|
||||
* @return IAgent 实例(仅 Route:xxx 场景),或 null(包括 Direct:、无匹配、异常等)
|
||||
*/
|
||||
public IAgent route() {
|
||||
try {
|
||||
// 1. 关键字快速匹配
|
||||
if (enableKeywordRouting) {
|
||||
String agentName = routingAgentRegistry.findAgentByKeyword(userQuery);
|
||||
if (agentName != null) {
|
||||
log.debug("关键字匹配命中 Agent: {}", agentName);
|
||||
return createAgent(agentName);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. LLM 智能路由
|
||||
if (enableLlmRouting) {
|
||||
String contextSummary = buildContextSummary(memoryPrompt);
|
||||
String agentDescriptions = routingAgentRegistry.getAgentDescriptions();
|
||||
String prompt = routingPromptTemplate
|
||||
.replace("{agent_descriptions}", agentDescriptions)
|
||||
.replace("{conversation_context}", contextSummary)
|
||||
.replace("{user_input}", userQuery);
|
||||
|
||||
String decision = chatModel.chat(prompt, chatOptions);
|
||||
|
||||
if (decision != null && decision.startsWith("Route:")) {
|
||||
String agentName = decision.substring("Route:".length()).trim();
|
||||
return createAgent(agentName);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 无有效 Route,返回 null
|
||||
log.debug("RouteAgent 未匹配到可路由的 Agent,返回 null。Query: {}", userQuery);
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("RouteAgent 路由异常,返回 null", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private IAgent createAgent(String agentName) {
|
||||
RoutingAgentFactory factory = routingAgentRegistry.getAgentFactory(agentName);
|
||||
if (factory == null) {
|
||||
log.warn("Agent 不存在: {}, 返回 null", agentName);
|
||||
return null;
|
||||
}
|
||||
return factory.create(chatModel, userQuery, memoryPrompt);
|
||||
}
|
||||
|
||||
private String buildContextSummary(MemoryPrompt history) {
|
||||
List<Message> messages = history.getMessages();
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return "(无历史对话)";
|
||||
}
|
||||
|
||||
int start = Math.max(0, messages.size() - 4);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = start; i < messages.size(); i++) {
|
||||
Message msg = messages.get(i);
|
||||
String role = msg instanceof AiMessage ? "AI" : "User";
|
||||
String content = msg.getTextContent() != null ? msg.getTextContent() : "";
|
||||
sb.append(role).append(": ").append(content.trim()).append("\n");
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
|
||||
public void setEnableKeywordRouting(boolean enable) {
|
||||
this.enableKeywordRouting = enable;
|
||||
}
|
||||
|
||||
public void setEnableLlmRouting(boolean enable) {
|
||||
this.enableLlmRouting = enable;
|
||||
}
|
||||
|
||||
public void setRoutingPromptTemplate(String routingPromptTemplate) {
|
||||
if (routingPromptTemplate != null && !routingPromptTemplate.trim().isEmpty()) {
|
||||
this.routingPromptTemplate = routingPromptTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
public void setChatOptions(ChatOptions chatOptions) {
|
||||
if (chatOptions != null) {
|
||||
this.chatOptions = chatOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String, RoutingAgentFactory> agentFactories = new HashMap<>();
|
||||
private final Map<String, String> agentDescriptions = new HashMap<>();
|
||||
private final Map<String, String> 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<String> 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<String, String> 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<String, String> entry : agentDescriptions.entrySet()) {
|
||||
sb.append("- ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<java.math.BigDecimal> {
|
||||
@Override
|
||||
public java.math.BigDecimal convert(String text) {
|
||||
return new java.math.BigDecimal(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<java.math.BigInteger> {
|
||||
@Override
|
||||
public java.math.BigInteger convert(String text) {
|
||||
return new java.math.BigInteger(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Boolean> {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<byte[]> {
|
||||
@Override
|
||||
public byte[] convert(String text) {
|
||||
return text.getBytes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Byte> {
|
||||
@Override
|
||||
public Byte convert(String text) {
|
||||
return Byte.parseByte(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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{
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Class<?>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Double> {
|
||||
@Override
|
||||
public Double convert(String text) {
|
||||
return Double.parseDouble(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Float> {
|
||||
@Override
|
||||
public Float convert(String text) {
|
||||
return Float.parseFloat(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<T> {
|
||||
|
||||
/**
|
||||
* convert the given text to type <T>.
|
||||
*
|
||||
* @param text the text to convert.
|
||||
* @return the convert value or null.
|
||||
*/
|
||||
T convert(String text) throws ConvertException;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Integer>{
|
||||
@Override
|
||||
public Integer convert(String text) throws ConvertException {
|
||||
return Integer.parseInt(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Long> {
|
||||
@Override
|
||||
public Long convert(String text) {
|
||||
return Long.parseLong(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Short>{
|
||||
@Override
|
||||
public Short convert(String text) throws ConvertException {
|
||||
return Short.parseShort(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Document> split(Document text, DocumentIdGenerator idGenerator);
|
||||
|
||||
default List<Document> split(Document text) {
|
||||
return split(text, null);
|
||||
}
|
||||
|
||||
default List<Document> splitAll(List<Document> documents, DocumentIdGenerator idGenerator) {
|
||||
if (documents == null || documents.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return documents.stream()
|
||||
.flatMap(document -> split(document, idGenerator).stream())
|
||||
.collect(toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Document> 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<String> 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<Document> fallbackDocs = fallbackSplitter.split(document, idGenerator);
|
||||
if (fallbackDocs.size() > maxChunks) {
|
||||
return new ArrayList<>(fallbackDocs.subList(0, maxChunks));
|
||||
}
|
||||
return fallbackDocs;
|
||||
}
|
||||
|
||||
List<String> validChunks = chunks.stream()
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.limit(maxChunks)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Document> 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<String> parseChunksBySeparator(String text, String separator) {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String[] parts = text.split(separator, -1);
|
||||
List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Document> 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<DocumentChunk> chunks = new ArrayList<>();
|
||||
Deque<HeaderInfo> 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<Document> 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<DocumentChunk> chunks, String content,
|
||||
Deque<HeaderInfo> headerStack, int startLine, int endLine, Document sourceDoc) {
|
||||
if (StringUtil.noText(content.trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从根到当前构建标题路径
|
||||
List<String> headerPath = new ArrayList<>();
|
||||
List<HeaderInfo> 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<String> headerPath;
|
||||
final int startLine;
|
||||
final int endLine;
|
||||
|
||||
DocumentChunk(String content, List<String> headerPath, int startLine, int endLine) {
|
||||
this.content = content;
|
||||
this.headerPath = headerPath;
|
||||
this.startLine = startLine;
|
||||
this.endLine = endLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Document> split(Document document, DocumentIdGenerator idGenerator) {
|
||||
if (document == null || StringUtil.noText(document.getContent())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] textArray = document.getContent().split(regex);
|
||||
List<Document> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Document> 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<Document> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Document> 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<Integer> tokens = encoding.encode(content).boxed();
|
||||
|
||||
|
||||
int index = 0, currentIndex = index;
|
||||
int maxIndex = tokens.size();
|
||||
|
||||
List<Document> chunks = new ArrayList<>();
|
||||
while (currentIndex < maxIndex) {
|
||||
int endIndex = Math.min(currentIndex + chunkSize, maxIndex);
|
||||
List<Integer> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<FileExtractor> 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<FileExtractor> 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<FileExtractor> extractors) {
|
||||
extractors.forEach(this::register);
|
||||
}
|
||||
|
||||
|
||||
public List<FileExtractor> findExtractors(DocumentSource source) {
|
||||
return extractors.stream()
|
||||
.filter(extractor -> extractor.supports(source))
|
||||
.sorted(FileExtractor.ORDER_COMPARATOR)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<FileExtractor> ORDER_COMPARATOR =
|
||||
Comparator.comparingInt(FileExtractor::getOrder);
|
||||
|
||||
/**
|
||||
* 判断该 Extractor 是否支持处理此文档
|
||||
*/
|
||||
boolean supports(DocumentSource source);
|
||||
|
||||
String extractText(DocumentSource source) throws IOException;
|
||||
|
||||
|
||||
default int getOrder() {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String> SUPPORTED_MIME_TYPES;
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS;
|
||||
|
||||
static {
|
||||
Set<String> mimeTypes = new HashSet<>();
|
||||
mimeTypes.add("application/msword");
|
||||
SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
|
||||
|
||||
Set<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String> KNOWN_MIME_TYPES;
|
||||
private static final String MIME_PREFIX = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS;
|
||||
|
||||
static {
|
||||
// 精确 MIME(可选)
|
||||
Set<String> mimeTypes = new HashSet<>();
|
||||
mimeTypes.add("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
KNOWN_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
|
||||
|
||||
// 支持的扩展名
|
||||
Set<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String> SUPPORTED_MIME_TYPES;
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS;
|
||||
|
||||
static {
|
||||
Set<String> mimeTypes = new HashSet<>();
|
||||
mimeTypes.add("text/html");
|
||||
mimeTypes.add("application/xhtml+xml");
|
||||
SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
|
||||
|
||||
Set<String> extensions = new HashSet<>();
|
||||
extensions.add("html");
|
||||
extensions.add("htm");
|
||||
extensions.add("xhtml");
|
||||
extensions.add("mhtml");
|
||||
SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
|
||||
}
|
||||
|
||||
// 噪音过滤规则
|
||||
private static final Set<String> 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<String> 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<String> CUSTOM_SELECTORS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> 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<String> 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<String> keywords = new HashSet<>(CLASS_KEYWORDS);
|
||||
keywords.addAll(CUSTOM_CLASS_KEYWORDS);
|
||||
|
||||
// 使用 DFS 遍历所有元素
|
||||
Deque<Element> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String> SUPPORTED_MIME_TYPES;
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS;
|
||||
|
||||
static {
|
||||
Set<String> mimeTypes = new HashSet<>();
|
||||
mimeTypes.add("application/pdf");
|
||||
SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
|
||||
|
||||
Set<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String> SUPPORTED_MIME_TYPES;
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS;
|
||||
|
||||
static {
|
||||
Set<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String> SUPPORTED_MIME_TYPES;
|
||||
private static final String MIME_PREFIX = "application/vnd.openxmlformats-officedocument.presentationml";
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS;
|
||||
|
||||
static {
|
||||
// 精确 MIME(可选)
|
||||
Set<String> 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<String> 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<XSLFSlide> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 从 HTTP/HTTPS URL 读取文档的输入源,支持缓存(避免重复请求)
|
||||
* 自动判断使用内存缓存还是临时文件缓存
|
||||
*/
|
||||
public class HttpDocumentSource implements DocumentSource {
|
||||
private static final int DEFAULT_CONNECT_TIMEOUT = 20_000;
|
||||
private static final int DEFAULT_READ_TIMEOUT = 60_000;
|
||||
private static final long MEMORY_THRESHOLD = 10 * 1024 * 1024; // 10MB 以内走内存
|
||||
|
||||
private final String url;
|
||||
private final String providedFileName;
|
||||
private final String mimeType;
|
||||
private final int connectTimeout;
|
||||
private final int readTimeout;
|
||||
private final java.util.function.Consumer<HttpURLConnection> connectionCustomizer;
|
||||
|
||||
private volatile byte[] cachedBytes = null;
|
||||
private volatile File tempFile = null;
|
||||
private volatile String resolvedFileName = null;
|
||||
private volatile String resolvedMimeType = null;
|
||||
private final AtomicBoolean downloaded = new AtomicBoolean(false);
|
||||
|
||||
public HttpDocumentSource(String url) {
|
||||
this(url, null, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, null);
|
||||
}
|
||||
|
||||
public HttpDocumentSource(String url, String fileName) {
|
||||
this(url, fileName, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, null);
|
||||
}
|
||||
|
||||
public HttpDocumentSource(String url, String fileName, String mimeType) {
|
||||
this(url, fileName, mimeType, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, null);
|
||||
}
|
||||
|
||||
public HttpDocumentSource(
|
||||
String url,
|
||||
String fileName,
|
||||
String mimeType,
|
||||
int connectTimeout,
|
||||
int readTimeout,
|
||||
java.util.function.Consumer<HttpURLConnection> connectionCustomizer
|
||||
) {
|
||||
this.url = validateUrl(url);
|
||||
this.providedFileName = fileName;
|
||||
this.mimeType = mimeType;
|
||||
this.connectTimeout = connectTimeout;
|
||||
this.readTimeout = readTimeout;
|
||||
this.connectionCustomizer = connectionCustomizer;
|
||||
}
|
||||
|
||||
private String validateUrl(String url) {
|
||||
try {
|
||||
new URL(url).toURI();
|
||||
return url;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Invalid URL: " + url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileName() {
|
||||
if (resolvedFileName != null) {
|
||||
return resolvedFileName;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (resolvedFileName == null) {
|
||||
resolvedFileName = detectFileName();
|
||||
}
|
||||
}
|
||||
return resolvedFileName;
|
||||
}
|
||||
|
||||
private String detectFileName() {
|
||||
// 1. 用户提供
|
||||
if (providedFileName != null && !providedFileName.trim().isEmpty()) {
|
||||
return sanitizeFileName(providedFileName);
|
||||
}
|
||||
|
||||
// 2. 从 URL 路径提取
|
||||
String fromUrl = extractFileNameFromUrl();
|
||||
if (fromUrl != null) return fromUrl;
|
||||
|
||||
// 3. 从 Content-Disposition 提取(需要连接)
|
||||
try {
|
||||
HttpURLConnection conn = createConnection();
|
||||
conn.setRequestMethod("HEAD"); // 只获取头
|
||||
conn.connect();
|
||||
String fromHeader = extractFileNameFromHeader(conn);
|
||||
conn.disconnect();
|
||||
if (fromHeader != null) return fromHeader;
|
||||
} catch (IOException e) {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
return "downloaded-file";
|
||||
}
|
||||
|
||||
private String extractFileNameFromUrl() {
|
||||
try {
|
||||
URL urlObj = new URL(this.url);
|
||||
String path = urlObj.getPath();
|
||||
if (path != null && path.length() > 1) {
|
||||
String name = Paths.get(path).getFileName().toString();
|
||||
if (name.contains(".")) {
|
||||
return sanitizeFileName(name);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractFileNameFromHeader(HttpURLConnection conn) {
|
||||
try {
|
||||
String header = conn.getHeaderField("Content-Disposition");
|
||||
if (header != null) {
|
||||
Pattern pattern = Pattern.compile("filename\\s*=\\s*\"?([^\";]+)\"?");
|
||||
Matcher matcher = pattern.matcher(header);
|
||||
if (matcher.find()) {
|
||||
return sanitizeFileName(matcher.group(1));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String sanitizeFileName(String filename) {
|
||||
if (filename == null) return "unknown";
|
||||
return filename
|
||||
.replaceAll("[\\\\/:*?\"<>|]", "_")
|
||||
.replaceAll("\\.\\.", "_")
|
||||
.replaceAll("^\\s+|\\s+$", "")
|
||||
.isEmpty() ? "file" : filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
if (resolvedMimeType != null) {
|
||||
return resolvedMimeType;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (resolvedMimeType == null) {
|
||||
try {
|
||||
HttpURLConnection conn = createConnection();
|
||||
conn.setRequestMethod("HEAD");
|
||||
conn.connect();
|
||||
resolvedMimeType = conn.getContentType();
|
||||
conn.disconnect();
|
||||
} catch (IOException e) {
|
||||
resolvedMimeType = mimeType; // fallback
|
||||
}
|
||||
if (resolvedMimeType == null) {
|
||||
resolvedMimeType = mimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolvedMimeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
downloadIfNeeded();
|
||||
if (cachedBytes != null) {
|
||||
return new ByteArrayInputStream(cachedBytes);
|
||||
} else if (tempFile != null) {
|
||||
return new FileInputStream(tempFile);
|
||||
} else {
|
||||
throw new IOException("No content available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载一次,缓存结果
|
||||
*/
|
||||
private void downloadIfNeeded() throws IOException {
|
||||
if (downloaded.get()) return;
|
||||
|
||||
synchronized (this) {
|
||||
if (downloaded.get()) return;
|
||||
|
||||
HttpURLConnection conn = createConnection();
|
||||
conn.connect();
|
||||
|
||||
try {
|
||||
int code = conn.getResponseCode();
|
||||
if (code >= 400) {
|
||||
throw new IOException("HTTP " + code + " from " + url);
|
||||
}
|
||||
|
||||
// 判断是否走内存 or 临时文件
|
||||
long contentLength = conn.getContentLengthLong();
|
||||
boolean useMemory = contentLength > 0 && contentLength <= MEMORY_THRESHOLD;
|
||||
|
||||
if (useMemory) {
|
||||
// 内存缓存
|
||||
this.cachedBytes = IOUtils.toByteArray(conn.getInputStream(), MEMORY_THRESHOLD);
|
||||
} else {
|
||||
// 临时文件缓存
|
||||
this.tempFile = File.createTempFile("http-", ".cache");
|
||||
this.tempFile.deleteOnExit();
|
||||
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copyStream(conn.getInputStream(), fos, Long.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 MIME(如果未指定)
|
||||
if (this.resolvedMimeType == null) {
|
||||
this.resolvedMimeType = conn.getContentType();
|
||||
}
|
||||
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
|
||||
downloaded.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpURLConnection createConnection() throws IOException {
|
||||
URL urlObj = new URL(this.url);
|
||||
HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
|
||||
conn.setConnectTimeout(connectTimeout);
|
||||
conn.setReadTimeout(readTimeout);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent", "DocumentParser/1.0");
|
||||
if (connectionCustomizer != null) {
|
||||
connectionCustomizer.accept(conn);
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存大小(用于调试)
|
||||
*/
|
||||
public long getCachedSize() {
|
||||
if (cachedBytes != null) return cachedBytes.length;
|
||||
if (tempFile != null) return tempFile.length();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (tempFile != null && tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* 将输入流保存到临时文件的 DocumentSource
|
||||
* 适用于大文件,避免内存溢出
|
||||
* 线程安全,文件名唯一,支持自动清理
|
||||
*/
|
||||
public class TemporaryFileStreamDocumentSource implements DocumentSource {
|
||||
private static final Logger log = Logger.getLogger(TemporaryFileStreamDocumentSource.class.getName());
|
||||
private static final long DEFAULT_MAX_SIZE = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
private final File tempFile;
|
||||
private final String fileName;
|
||||
private final String mimeType;
|
||||
|
||||
/**
|
||||
* 创建临时文件源(默认最大 100MB)
|
||||
*/
|
||||
public TemporaryFileStreamDocumentSource(InputStream inputStream, String fileName, String mimeType) throws IOException {
|
||||
this(inputStream, fileName, mimeType, DEFAULT_MAX_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时文件源(可指定最大大小)
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @param fileName 建议文件名(用于日志和扩展名推断)
|
||||
* @param mimeType MIME 类型(可选)
|
||||
* @param maxSize 最大允许大小(字节)
|
||||
* @throws IOException 文件过大或 I/O 错误
|
||||
*/
|
||||
public TemporaryFileStreamDocumentSource(
|
||||
InputStream inputStream,
|
||||
String fileName,
|
||||
String mimeType,
|
||||
long maxSize) throws IOException {
|
||||
|
||||
Objects.requireNonNull(inputStream, "InputStream cannot be null");
|
||||
|
||||
this.fileName = sanitizeFileName(fileName);
|
||||
this.mimeType = mimeType;
|
||||
|
||||
// 推断后缀(用于调试)
|
||||
String suffix = inferSuffix(this.fileName);
|
||||
|
||||
// 创建唯一临时文件
|
||||
this.tempFile = File.createTempFile("doc-", suffix);
|
||||
this.tempFile.deleteOnExit(); // JVM 退出时清理
|
||||
|
||||
log.info("Creating temp file for " + this.fileName + ": " + tempFile.getAbsolutePath());
|
||||
|
||||
// 复制流(带大小限制)
|
||||
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copyStream(inputStream, fos, maxSize);
|
||||
} catch (IOException e) {
|
||||
// 清理失败的临时文件
|
||||
boolean deleted = tempFile.delete();
|
||||
log.warning("Failed to write temp file, deleted: " + deleted);
|
||||
throw e;
|
||||
}
|
||||
|
||||
log.fine("Temp file created: " + tempFile.length() + " bytes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public InputStream openStream() throws IOException {
|
||||
if (!tempFile.exists()) {
|
||||
throw new FileNotFoundException("Temp file not found: " + tempFile.getAbsolutePath());
|
||||
}
|
||||
return Files.newInputStream(tempFile.toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (tempFile.exists()) {
|
||||
boolean deleted = tempFile.delete();
|
||||
if (!deleted) {
|
||||
log.warning("Failed to delete temp file: " + tempFile.getAbsolutePath());
|
||||
} else {
|
||||
log.fine("Cleaned up temp file: " + tempFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================
|
||||
// 工具方法
|
||||
// ========================
|
||||
|
||||
/**
|
||||
* 推断文件后缀(用于临时文件命名,便于调试)
|
||||
*/
|
||||
private String inferSuffix(String fileName) {
|
||||
if (fileName == null || !fileName.contains(".")) {
|
||||
return ".tmp";
|
||||
}
|
||||
int lastDot = fileName.lastIndexOf('.');
|
||||
String ext = fileName.substring(lastDot); // 包含 .
|
||||
if (ext.length() > 1 && ext.length() <= 10 && ext.matches("\\.[a-zA-Z0-9]{1,10}")) {
|
||||
return ext;
|
||||
}
|
||||
return ".tmp";
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理文件名中的非法字符
|
||||
*/
|
||||
private String sanitizeFileName(String fileName) {
|
||||
if (fileName == null) return "unknown";
|
||||
return fileName
|
||||
.replaceAll("[\\\\/:*?\"<>|]", "_")
|
||||
.replaceAll("\\.\\.", "_")
|
||||
.replaceAll("^\\s+|\\s+$", "")
|
||||
.isEmpty() ? "file" : fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Message> getMessages(int count);
|
||||
|
||||
void addMessage(Message message);
|
||||
|
||||
default void addMessages(Collection<Message> messages){
|
||||
for (Message message : messages) {
|
||||
addMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void clear();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<Message> 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<Message> 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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;
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<T extends AbstractTextMessage<?>>
|
||||
extends Message implements Copyable<T> {
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.StringUtil;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AiMessage extends AbstractTextMessage<AiMessage> {
|
||||
|
||||
private Integer index;
|
||||
private Integer promptTokens;
|
||||
private Integer completionTokens;
|
||||
private Integer totalTokens;
|
||||
private Integer localPromptTokens;
|
||||
private Integer localCompletionTokens;
|
||||
private Integer localTotalTokens;
|
||||
private String reasoningContent;
|
||||
private List<ToolCall> toolCalls;
|
||||
|
||||
private String fullContent;
|
||||
private String fullReasoningContent;
|
||||
|
||||
/**
|
||||
* LLM 响应结束的原因(如 "stop", "length", "tool_calls" 等),
|
||||
* 符合 OpenAI 等主流 API 的 finish_reason 语义。
|
||||
*/
|
||||
private String finishReason;
|
||||
|
||||
// 同 reasoningContent,只是某些框架会返回这个字段,而不是 finishReason
|
||||
private String stopReason;
|
||||
|
||||
private Boolean finished;
|
||||
|
||||
public AiMessage() {
|
||||
super();
|
||||
}
|
||||
|
||||
public AiMessage(String content) {
|
||||
this.fullContent = content;
|
||||
}
|
||||
|
||||
public void merge(AiMessage delta) {
|
||||
if (delta.content != null) {
|
||||
if (this.content == null) this.content = "";
|
||||
this.content += delta.content;
|
||||
this.fullContent = this.content;
|
||||
}
|
||||
|
||||
if (delta.reasoningContent != null) {
|
||||
if (this.reasoningContent == null) this.reasoningContent = "";
|
||||
this.reasoningContent += delta.reasoningContent;
|
||||
this.fullReasoningContent = this.reasoningContent;
|
||||
}
|
||||
|
||||
if (delta.toolCalls != null && !delta.toolCalls.isEmpty()) {
|
||||
if (this.toolCalls == null) this.toolCalls = new ArrayList<>();
|
||||
mergeToolCalls(delta.toolCalls);
|
||||
}
|
||||
if (delta.index != null) this.index = delta.index;
|
||||
if (delta.promptTokens != null) this.promptTokens = delta.promptTokens;
|
||||
if (delta.completionTokens != null) this.completionTokens = delta.completionTokens;
|
||||
if (delta.totalTokens != null) this.totalTokens = delta.totalTokens;
|
||||
if (delta.localPromptTokens != null) this.localPromptTokens = delta.localPromptTokens;
|
||||
if (delta.localCompletionTokens != null) this.localCompletionTokens = delta.localCompletionTokens;
|
||||
if (delta.localTotalTokens != null) this.localTotalTokens = delta.localTotalTokens;
|
||||
if (delta.finishReason != null) this.finishReason = delta.finishReason;
|
||||
if (delta.stopReason != null) this.stopReason = delta.stopReason;
|
||||
}
|
||||
|
||||
private void mergeToolCalls(List<ToolCall> deltaCalls) {
|
||||
if (deltaCalls == null || deltaCalls.isEmpty()) return;
|
||||
|
||||
if (this.toolCalls == null || this.toolCalls.isEmpty()) {
|
||||
this.toolCalls = new ArrayList<>(deltaCalls);
|
||||
return;
|
||||
}
|
||||
|
||||
ToolCall lastCall = this.toolCalls.get(this.toolCalls.size() - 1);
|
||||
|
||||
// 正常情况下 delta 部分只有 1 条
|
||||
ToolCall deltaCall = deltaCalls.get(0);
|
||||
|
||||
// 新增
|
||||
if (isNewCall(deltaCall, lastCall)) {
|
||||
this.toolCalls.add(deltaCall);
|
||||
}
|
||||
// 合并
|
||||
else {
|
||||
mergeSingleCall(lastCall, deltaCall);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNewCall(ToolCall deltaCall, ToolCall lastCall) {
|
||||
if (StringUtil.noText(deltaCall.getId()) && StringUtil.noText(deltaCall.getName())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtil.hasText(deltaCall.getId())) {
|
||||
return !deltaCall.getId().equals(lastCall.getId());
|
||||
}
|
||||
|
||||
if (StringUtil.hasText(deltaCall.getName())) {
|
||||
return !deltaCall.getName().equals(lastCall.getName());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void mergeSingleCall(ToolCall existing, ToolCall delta) {
|
||||
if (delta.getArguments() != null) {
|
||||
if (existing.getArguments() == null) {
|
||||
existing.setArguments("");
|
||||
}
|
||||
existing.setArguments(existing.getArguments() + delta.getArguments());
|
||||
}
|
||||
if (StringUtil.hasText(delta.getId())) {
|
||||
existing.setId(delta.getId());
|
||||
}
|
||||
if (StringUtil.hasText(delta.getName())) {
|
||||
existing.setName(delta.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Getters & Setters (保持原有不变) =====
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public Integer getPromptTokens() {
|
||||
return promptTokens;
|
||||
}
|
||||
|
||||
public void setPromptTokens(Integer promptTokens) {
|
||||
this.promptTokens = promptTokens;
|
||||
}
|
||||
|
||||
public Integer getCompletionTokens() {
|
||||
return completionTokens;
|
||||
}
|
||||
|
||||
public void setCompletionTokens(Integer completionTokens) {
|
||||
this.completionTokens = completionTokens;
|
||||
}
|
||||
|
||||
public Integer getTotalTokens() {
|
||||
return totalTokens;
|
||||
}
|
||||
|
||||
public void setTotalTokens(Integer totalTokens) {
|
||||
this.totalTokens = totalTokens;
|
||||
}
|
||||
|
||||
public Integer getLocalPromptTokens() {
|
||||
return localPromptTokens;
|
||||
}
|
||||
|
||||
public void setLocalPromptTokens(Integer localPromptTokens) {
|
||||
this.localPromptTokens = localPromptTokens;
|
||||
}
|
||||
|
||||
public Integer getLocalCompletionTokens() {
|
||||
return localCompletionTokens;
|
||||
}
|
||||
|
||||
public void setLocalCompletionTokens(Integer localCompletionTokens) {
|
||||
this.localCompletionTokens = localCompletionTokens;
|
||||
}
|
||||
|
||||
public Integer getLocalTotalTokens() {
|
||||
return localTotalTokens;
|
||||
}
|
||||
|
||||
public void setLocalTotalTokens(Integer localTotalTokens) {
|
||||
this.localTotalTokens = localTotalTokens;
|
||||
}
|
||||
|
||||
public String getFullContent() {
|
||||
return fullContent;
|
||||
}
|
||||
|
||||
public void setFullContent(String fullContent) {
|
||||
this.fullContent = fullContent;
|
||||
}
|
||||
|
||||
public String getReasoningContent() {
|
||||
return reasoningContent;
|
||||
}
|
||||
|
||||
public void setReasoningContent(String reasoningContent) {
|
||||
this.reasoningContent = reasoningContent;
|
||||
}
|
||||
|
||||
public String getFinishReason() {
|
||||
return finishReason;
|
||||
}
|
||||
|
||||
public void setFinishReason(String finishReason) {
|
||||
this.finishReason = finishReason;
|
||||
}
|
||||
|
||||
public String getStopReason() {
|
||||
return stopReason;
|
||||
}
|
||||
|
||||
public void setStopReason(String stopReason) {
|
||||
this.stopReason = stopReason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTextContent() {
|
||||
return fullContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并返回当前对象的副本。
|
||||
*
|
||||
* @return 一个新的、内容相同但内存独立的对象
|
||||
*/
|
||||
@Override
|
||||
public AiMessage copy() {
|
||||
AiMessage copy = new AiMessage();
|
||||
// 基本字段
|
||||
copy.content = this.content;
|
||||
copy.fullContent = this.fullContent;
|
||||
copy.reasoningContent = this.reasoningContent;
|
||||
copy.fullReasoningContent = this.fullReasoningContent;
|
||||
copy.finishReason = this.finishReason;
|
||||
copy.stopReason = this.stopReason;
|
||||
copy.finished = this.finished;
|
||||
|
||||
// Token 字段
|
||||
copy.index = this.index;
|
||||
copy.promptTokens = this.promptTokens;
|
||||
copy.completionTokens = this.completionTokens;
|
||||
copy.totalTokens = this.totalTokens;
|
||||
copy.localPromptTokens = this.localPromptTokens;
|
||||
copy.localCompletionTokens = this.localCompletionTokens;
|
||||
copy.localTotalTokens = this.localTotalTokens;
|
||||
|
||||
// ToolCalls: 深拷贝 List 和每个 ToolCall
|
||||
if (this.toolCalls != null) {
|
||||
copy.toolCalls = new ArrayList<>();
|
||||
for (ToolCall tc : this.toolCalls) {
|
||||
if (tc != null) {
|
||||
copy.toolCalls.add(tc.copy());
|
||||
} else {
|
||||
copy.toolCalls.add(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata
|
||||
if (this.metadataMap != null) {
|
||||
copy.metadataMap = new HashMap<>(this.metadataMap);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public List<ToolCall> getToolCalls() {
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(List<ToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
|
||||
public String getFullReasoningContent() {
|
||||
return fullReasoningContent;
|
||||
}
|
||||
|
||||
public void setFullReasoningContent(String fullReasoningContent) {
|
||||
this.fullReasoningContent = fullReasoningContent;
|
||||
}
|
||||
|
||||
public int getEffectiveTotalTokens() {
|
||||
if (this.totalTokens != null) return this.totalTokens;
|
||||
if (this.promptTokens != null && this.completionTokens != null) {
|
||||
return this.promptTokens + this.completionTokens;
|
||||
}
|
||||
if (this.localTotalTokens != null) return this.localTotalTokens;
|
||||
if (this.localPromptTokens != null && this.localCompletionTokens != null) {
|
||||
return this.localPromptTokens + this.localCompletionTokens;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Boolean getFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
public void setFinished(Boolean finished) {
|
||||
this.finished = finished;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断当前对象是否为最终的 delta 对象。
|
||||
*
|
||||
* @return true 表示当前对象为最终的 delta 对象,否则为 false
|
||||
*/
|
||||
public boolean isFinalDelta() {
|
||||
return (finished != null && finished);
|
||||
}
|
||||
|
||||
public boolean hasFinishOrStopReason() {
|
||||
return StringUtil.hasText(this.finishReason)
|
||||
|| StringUtil.hasText(this.stopReason);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AiMessage{" +
|
||||
"index=" + index +
|
||||
", promptTokens=" + promptTokens +
|
||||
", completionTokens=" + completionTokens +
|
||||
", totalTokens=" + totalTokens +
|
||||
", localPromptTokens=" + localPromptTokens +
|
||||
", localCompletionTokens=" + localCompletionTokens +
|
||||
", localTotalTokens=" + localTotalTokens +
|
||||
", reasoningContent='" + reasoningContent + '\'' +
|
||||
", toolCalls=" + toolCalls +
|
||||
", fullContent='" + fullContent + '\'' +
|
||||
", fullReasoningContent='" + fullReasoningContent + '\'' +
|
||||
", finishReason='" + finishReason + '\'' +
|
||||
", stopReason='" + stopReason + '\'' +
|
||||
", finished=" + finished +
|
||||
", content='" + content + '\'' +
|
||||
", metadataMap=" + metadataMap +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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)进行交互。
|
||||
* 消息内容可以是纯文本,也可以是多模态内容(例如:文本 + 图像等)。
|
||||
*
|
||||
* <p>该类继承自 {@link Metadata},允许附加任意元数据(如来源、时间戳、追踪ID等)。
|
||||
*
|
||||
* @see #getTextContent()
|
||||
*/
|
||||
public abstract class Message extends Metadata {
|
||||
|
||||
/**
|
||||
* 提取消息中的纯文本部分。
|
||||
*
|
||||
* <p>无论原始内容是纯文本还是多模态结构(如文本+图像),本方法应返回其中所有文本内容的合理合并结果。
|
||||
* 例如,在 OpenAI 多模态消息中,应遍历所有 {@code content} 元素,提取类型为 {@code text} 的部分并拼接。
|
||||
*
|
||||
* <p>返回的字符串应不包含非文本元素(如图像、音频等),且应保持原始文本的语义顺序(如适用)。
|
||||
* 若消息中无文本内容,则返回空字符串({@code ""}),而非 {@code null}。
|
||||
*
|
||||
* <p>该方法主要用于日志记录、监控、文本分析等仅需文本语义的场景。
|
||||
*
|
||||
* @return 消息中提取出的纯文本内容。
|
||||
*/
|
||||
public abstract String getTextContent();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<SystemMessage> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<ToolCall> {
|
||||
|
||||
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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<ToolMessage> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<UserMessage> {
|
||||
|
||||
private List<String> audioUrls;
|
||||
private List<String> videoUrls;
|
||||
private List<String> imageUrls;
|
||||
private List<Tool> 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<Tool> getTools() {
|
||||
return tools;
|
||||
}
|
||||
|
||||
public Map<String, Tool> getToolsMap() {
|
||||
if (tools == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Tool> 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<String> getAudioUrls() {
|
||||
return audioUrls;
|
||||
}
|
||||
|
||||
public void setAudioUrls(List<String> audioUrls) {
|
||||
this.audioUrls = audioUrls;
|
||||
}
|
||||
|
||||
public void addAudioUrl(String audioUrl) {
|
||||
if (audioUrls == null) {
|
||||
audioUrls = new ArrayList<>(1);
|
||||
}
|
||||
audioUrls.add(audioUrl);
|
||||
}
|
||||
|
||||
|
||||
/// /// Video
|
||||
public List<String> getVideoUrls() {
|
||||
return videoUrls;
|
||||
}
|
||||
|
||||
public void setVideoUrls(List<String> videoUrls) {
|
||||
this.videoUrls = videoUrls;
|
||||
}
|
||||
|
||||
public void addVideoUrl(String videoUrl) {
|
||||
if (videoUrls == null) {
|
||||
videoUrls = new ArrayList<>(1);
|
||||
}
|
||||
videoUrls.add(videoUrl);
|
||||
}
|
||||
|
||||
|
||||
/// /// Images
|
||||
public List<String> getImageUrls() {
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
public List<String> getImageUrlsForChat(ChatConfig config) {
|
||||
if (this.imageUrls == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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;
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 支持责任链、统一上下文和协议客户端的聊天模型基类。
|
||||
* <p>
|
||||
* 该类为所有具体的 LLM 实现(如 OpenAI、Qwen、Ollama)提供统一入口,并集成:
|
||||
* <ul>
|
||||
* <li><b>责任链模式</b>:通过 {@link ChatInterceptor} 实现请求拦截、监控、日志等横切逻辑</li>
|
||||
* <li><b>线程上下文管理</b>:通过 {@link ChatContextHolder} 在整个调用链中传递上下文信息</li>
|
||||
* <li><b>协议执行抽象</b>:通过 {@link ChatClient} 解耦协议细节,支持 HTTP/gRPC/WebSocket 等</li>
|
||||
* <li><b>可观测性</b>:自动集成 OpenTelemetry(通过 {@link ChatObservabilityInterceptor})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>架构流程</h2>
|
||||
* <ol>
|
||||
* <li>调用 {@link #chat(Prompt, ChatOptions)} 或 {@link #chatStream(Prompt, StreamResponseListener, ChatOptions)}</li>
|
||||
* <li>构建请求上下文(URL/Headers/Body)并初始化 {@link ChatContext}</li>
|
||||
* <li>构建责任链:可观测性拦截器 → 全局拦截器 → 用户拦截器</li>
|
||||
* <li>责任链执行:每个拦截器可修改 {@link ChatContext},最后由 {@link ChatClient} 执行实际调用</li>
|
||||
* <li>结果返回给调用方</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param <T> 具体的配置类型,必须是 {@link ChatConfig} 的子类
|
||||
*/
|
||||
public abstract class BaseChatModel<T extends ChatConfig> implements ChatModel {
|
||||
|
||||
/**
|
||||
* 聊天模型配置,包含 API Key、Endpoint、Model 等信息
|
||||
*/
|
||||
protected final T config;
|
||||
protected ChatClient chatClient;
|
||||
protected ChatRequestSpecBuilder chatRequestSpecBuilder;
|
||||
|
||||
/**
|
||||
* 拦截器链,按执行顺序存储(可观测性 → 全局 → 用户)
|
||||
*/
|
||||
private final List<ChatInterceptor> interceptors;
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,不使用实例级拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
*/
|
||||
public BaseChatModel(T config) {
|
||||
this(config, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个聊天模型实例,并指定实例级拦截器。
|
||||
* <p>
|
||||
* 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
|
||||
* 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
|
||||
*
|
||||
* @param config 聊天模型配置
|
||||
* @param userInterceptors 实例级拦截器列表
|
||||
*/
|
||||
public BaseChatModel(T config, List<ChatInterceptor> userInterceptors) {
|
||||
this.config = config;
|
||||
this.interceptors = buildInterceptorChain(userInterceptors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的拦截器链。
|
||||
* <p>
|
||||
* 执行顺序:
|
||||
* 1. 可观测性拦截器(最外层,最早执行)
|
||||
* 2. 全局拦截器(通过 GlobalChatInterceptors 注册)
|
||||
* 3. 用户拦截器(实例级)
|
||||
*
|
||||
* @param userInterceptors 用户提供的拦截器列表
|
||||
* @return 按执行顺序排列的拦截器链
|
||||
*/
|
||||
private List<ChatInterceptor> buildInterceptorChain(List<ChatInterceptor> userInterceptors) {
|
||||
List<ChatInterceptor> chain = new ArrayList<>();
|
||||
|
||||
// 1. 可观测性拦截器(最外层)
|
||||
// 仅在配置启用时添加,负责 OpenTelemetry 追踪和指标上报
|
||||
if (config.isObservabilityEnabled()) {
|
||||
chain.add(new ChatObservabilityInterceptor());
|
||||
}
|
||||
|
||||
// 2. 全局拦截器(通过 GlobalChatInterceptors 注册)
|
||||
// 适用于所有聊天模型实例的通用逻辑(如全局日志、认证)
|
||||
chain.addAll(GlobalChatInterceptors.getInterceptors());
|
||||
|
||||
// 3. 用户拦截器(实例级)
|
||||
// 适用于当前实例的特定逻辑
|
||||
if (userInterceptors != null) {
|
||||
chain.addAll(userInterceptors);
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行同步聊天请求。
|
||||
* <p>
|
||||
* 流程:
|
||||
* 1. 构建请求上下文(URL/Headers/Body)
|
||||
* 2. 初始化线程上下文 {@link ChatContext}
|
||||
* 3. 构建并执行责任链
|
||||
* 4. 返回 LLM 响应
|
||||
*
|
||||
* @param prompt 用户输入的提示
|
||||
* @param options 聊天选项(如流式开关、超时等)
|
||||
* @return LLM 响应结果
|
||||
*/
|
||||
@Override
|
||||
public AiMessageResponse chat(Prompt prompt, ChatOptions options) {
|
||||
if (options == null) {
|
||||
options = new ChatOptions();
|
||||
}
|
||||
// 强制关闭流式
|
||||
options.setStreaming(false);
|
||||
|
||||
|
||||
ChatRequestSpec request = getChatRequestSpecBuilder().buildRequest(prompt, options, config);
|
||||
|
||||
// 初始化聊天上下文(自动清理)
|
||||
try (ChatContextHolder.ChatContextScope scope =
|
||||
ChatContextHolder.beginChat(prompt, options, request, config)) {
|
||||
// 构建同步责任链并执行
|
||||
SyncChain chain = buildSyncChain(0);
|
||||
return chain.proceed(this, scope.context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流式聊天请求。
|
||||
* <p>
|
||||
* 流程与同步请求类似,但返回结果通过回调方式分片返回。
|
||||
*
|
||||
* @param prompt 用户输入的提示
|
||||
* @param listener 流式响应监听器
|
||||
* @param options 聊天选项
|
||||
*/
|
||||
@Override
|
||||
public void chatStream(Prompt prompt, StreamResponseListener listener, ChatOptions options) {
|
||||
if (options == null) {
|
||||
options = new ChatOptions();
|
||||
}
|
||||
options.setStreaming(true);
|
||||
|
||||
ChatRequestSpec request = getChatRequestSpecBuilder().buildRequest(prompt, options, config);
|
||||
|
||||
try (ChatContextHolder.ChatContextScope scope =
|
||||
ChatContextHolder.beginChat(prompt, options, request, config)) {
|
||||
|
||||
StreamChain chain = buildStreamChain(0);
|
||||
chain.proceed(this, scope.context, listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建同步责任链。
|
||||
* <p>
|
||||
* 递归构建拦截器链,链尾节点负责创建并调用 {@link ChatClient}。
|
||||
*
|
||||
* @param index 当前拦截器索引
|
||||
* @return 同步责任链
|
||||
*/
|
||||
private SyncChain buildSyncChain(int index) {
|
||||
// 链尾:执行实际 LLM 调用
|
||||
if (index >= interceptors.size()) {
|
||||
return (model, context) -> {
|
||||
AiMessageResponse aiMessageResponse = null;
|
||||
try {
|
||||
ChatMessageLogger.logRequest(model.getConfig(), context.getRequestSpec().getBody());
|
||||
aiMessageResponse = getChatClient().chat();
|
||||
return aiMessageResponse;
|
||||
} finally {
|
||||
ChatMessageLogger.logResponse(model.getConfig(), aiMessageResponse == null ? "" : aiMessageResponse.getRawText());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 递归构建下一个节点
|
||||
ChatInterceptor current = interceptors.get(index);
|
||||
SyncChain next = buildSyncChain(index + 1);
|
||||
|
||||
// 当前节点:执行拦截器逻辑
|
||||
return (model, context) -> current.intercept(model, context, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建流式责任链。
|
||||
* <p>
|
||||
* 与同步链类似,但支持流式监听器。
|
||||
*
|
||||
* @param index 当前拦截器索引
|
||||
* @return 流式责任链
|
||||
*/
|
||||
private StreamChain buildStreamChain(int index) {
|
||||
if (index >= interceptors.size()) {
|
||||
return (model, context, listener) -> {
|
||||
getChatClient().chatStream(listener);
|
||||
};
|
||||
}
|
||||
|
||||
ChatInterceptor current = interceptors.get(index);
|
||||
StreamChain next = buildStreamChain(index + 1);
|
||||
return (model, context, listener) -> current.interceptStream(model, context, listener, next);
|
||||
}
|
||||
|
||||
|
||||
public T getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public ChatClient getChatClient() {
|
||||
return chatClient;
|
||||
}
|
||||
|
||||
public void setChatClient(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
public ChatRequestSpecBuilder getChatRequestSpecBuilder() {
|
||||
return chatRequestSpecBuilder;
|
||||
}
|
||||
|
||||
public void setChatRequestSpecBuilder(ChatRequestSpecBuilder chatRequestSpecBuilder) {
|
||||
this.chatRequestSpecBuilder = chatRequestSpecBuilder;
|
||||
}
|
||||
|
||||
public List<ChatInterceptor> getInterceptors() {
|
||||
return interceptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态添加拦截器。
|
||||
* <p>
|
||||
* 新拦截器会被添加到链的末尾(在用户拦截器区域)。
|
||||
*
|
||||
* @param interceptor 要添加的拦截器
|
||||
*/
|
||||
public void addInterceptor(ChatInterceptor interceptor) {
|
||||
interceptors.add(interceptor);
|
||||
}
|
||||
|
||||
public void addInterceptor(int index, ChatInterceptor interceptor) {
|
||||
interceptors.add(index, interceptor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.config.BaseModelConfig;
|
||||
|
||||
public class ChatConfig extends BaseModelConfig {
|
||||
|
||||
protected Boolean supportImage;
|
||||
protected Boolean supportImageBase64Only; // 某些模型仅支持 base64 格式图片,比如 Ollama 部署的模型,或者某些本地化模型
|
||||
protected Boolean supportAudio;
|
||||
protected Boolean supportVideo;
|
||||
protected Boolean supportTool;
|
||||
protected Boolean supportToolMessage;
|
||||
protected Boolean supportThinking;
|
||||
|
||||
// 在调用工具的时候,是否需要推理结果作为 reasoning_content 传给大模型, 比如 Deepseek
|
||||
// 参考文档: https://api-docs.deepseek.com/zh-cn/guides/thinking_mode#%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8
|
||||
protected Boolean needReasoningContentForToolMessage;
|
||||
|
||||
protected boolean observabilityEnabled = true; // 默认开启
|
||||
protected boolean thinkingEnabled = false; // 默认关闭
|
||||
protected String thinkingProtocol = "none"; // "deepseek" "qwen" "ollama" "none"
|
||||
protected boolean logEnabled = true;
|
||||
|
||||
|
||||
protected boolean retryEnabled = true; // 默认开启错误重试
|
||||
protected int retryCount = 3;
|
||||
protected int retryInitialDelayMs = 1000;
|
||||
|
||||
|
||||
public boolean isLogEnabled() {
|
||||
return logEnabled;
|
||||
}
|
||||
|
||||
public void setLogEnabled(boolean logEnabled) {
|
||||
this.logEnabled = logEnabled;
|
||||
}
|
||||
|
||||
|
||||
public Boolean getSupportImage() {
|
||||
return supportImage;
|
||||
}
|
||||
|
||||
public void setSupportImage(Boolean supportImage) {
|
||||
this.supportImage = supportImage;
|
||||
}
|
||||
|
||||
public boolean isSupportImage() {
|
||||
return supportImage == null || supportImage;
|
||||
}
|
||||
|
||||
public Boolean getSupportImageBase64Only() {
|
||||
return supportImageBase64Only;
|
||||
}
|
||||
|
||||
public void setSupportImageBase64Only(Boolean supportImageBase64Only) {
|
||||
this.supportImageBase64Only = supportImageBase64Only;
|
||||
}
|
||||
|
||||
public boolean isSupportImageBase64Only() {
|
||||
return supportImageBase64Only != null && supportImageBase64Only;
|
||||
}
|
||||
|
||||
public Boolean getSupportAudio() {
|
||||
return supportAudio;
|
||||
}
|
||||
|
||||
public void setSupportAudio(Boolean supportAudio) {
|
||||
this.supportAudio = supportAudio;
|
||||
}
|
||||
|
||||
public boolean isSupportAudio() {
|
||||
return supportAudio == null || supportAudio;
|
||||
}
|
||||
|
||||
public Boolean getSupportVideo() {
|
||||
return supportVideo;
|
||||
}
|
||||
|
||||
public void setSupportVideo(Boolean supportVideo) {
|
||||
this.supportVideo = supportVideo;
|
||||
}
|
||||
|
||||
public boolean isSupportVideo() {
|
||||
return supportVideo == null || supportVideo;
|
||||
}
|
||||
|
||||
public void setSupportTool(Boolean supportTool) {
|
||||
this.supportTool = supportTool;
|
||||
}
|
||||
|
||||
public Boolean getSupportTool() {
|
||||
return supportTool;
|
||||
}
|
||||
|
||||
public boolean isSupportTool() {
|
||||
return supportTool == null || supportTool;
|
||||
}
|
||||
|
||||
public Boolean getSupportToolMessage() {
|
||||
return supportToolMessage;
|
||||
}
|
||||
|
||||
public void setSupportToolMessage(Boolean supportToolMessage) {
|
||||
this.supportToolMessage = supportToolMessage;
|
||||
}
|
||||
|
||||
public boolean isSupportToolMessage() {
|
||||
return supportToolMessage == null || supportToolMessage;
|
||||
}
|
||||
|
||||
public Boolean getSupportThinking() {
|
||||
return supportThinking;
|
||||
}
|
||||
|
||||
public void setSupportThinking(Boolean supportThinking) {
|
||||
this.supportThinking = supportThinking;
|
||||
}
|
||||
|
||||
public boolean isSupportThinking() {
|
||||
return supportThinking == null || supportThinking;
|
||||
}
|
||||
|
||||
public Boolean getNeedReasoningContentForToolMessage() {
|
||||
return needReasoningContentForToolMessage;
|
||||
}
|
||||
|
||||
public void setNeedReasoningContentForToolMessage(Boolean needReasoningContentForToolMessage) {
|
||||
this.needReasoningContentForToolMessage = needReasoningContentForToolMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要推理结果作为 reasoning_content 传给大模型
|
||||
* 比如 Deepseek 在工具调佣的时候,需要推理结果作为 reasoning_content 传给大模型
|
||||
*
|
||||
* @return 默认值为 false
|
||||
*/
|
||||
public boolean isNeedReasoningContentForToolMessage() {
|
||||
return needReasoningContentForToolMessage != null && needReasoningContentForToolMessage;
|
||||
}
|
||||
|
||||
public boolean isThinkingEnabled() {
|
||||
return thinkingEnabled;
|
||||
}
|
||||
|
||||
public void setThinkingEnabled(boolean thinkingEnabled) {
|
||||
this.thinkingEnabled = thinkingEnabled;
|
||||
}
|
||||
|
||||
public String getThinkingProtocol() {
|
||||
return thinkingProtocol;
|
||||
}
|
||||
|
||||
public void setThinkingProtocol(String thinkingProtocol) {
|
||||
this.thinkingProtocol = thinkingProtocol;
|
||||
}
|
||||
|
||||
public boolean isObservabilityEnabled() {
|
||||
return observabilityEnabled;
|
||||
}
|
||||
|
||||
public void setObservabilityEnabled(boolean observabilityEnabled) {
|
||||
this.observabilityEnabled = observabilityEnabled;
|
||||
}
|
||||
|
||||
public boolean isRetryEnabled() {
|
||||
return retryEnabled;
|
||||
}
|
||||
|
||||
public void setRetryEnabled(boolean retryEnabled) {
|
||||
this.retryEnabled = retryEnabled;
|
||||
}
|
||||
|
||||
public int getRetryCount() {
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public void setRetryCount(int retryCount) {
|
||||
this.retryCount = retryCount;
|
||||
}
|
||||
|
||||
public int getRetryInitialDelayMs() {
|
||||
return retryInitialDelayMs;
|
||||
}
|
||||
|
||||
public void setRetryInitialDelayMs(int retryInitialDelayMs) {
|
||||
this.retryInitialDelayMs = retryInitialDelayMs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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<String, Object> 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<String, Object> 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<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user