Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
19 KiB
JSON
1 line
19 KiB
JSON
{"repo": "bigchuidw3/spring-ai-agentx", "files": {"spring-ai-agentx-samples/src/main/java/com/agentx/ai/samples/TestConfig.java": {"74206edcf6fb207f25bbc52594e033d681f9f2d9": {"t": 1786068550.2060003, "n": 18306, "h": "cc7f7a49254e52b7", "c": "package com.agentx.ai.samples;\n\nimport com.agentx.ai.core.agent.ReactAgent;\nimport com.agentx.ai.core.chatmodels.DeepSeekV4ChatModel;\nimport com.agentx.ai.core.model.AgentStreamEvent;\nimport com.agentx.ai.core.model.PauseState;\nimport com.agentx.ai.core.model.RunnableParams;\nimport com.agentx.ai.core.model.SubAgentSource;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.springframework.ai.chat.model.ChatModel;\nimport org.springframework.ai.deepseek.DeepSeekChatOptions;\nimport org.springframework.ai.deepseek.api.DeepSeekApi;\nimport org.springframework.ai.document.MetadataMode;\nimport org.springframework.ai.embedding.EmbeddingModel;\nimport org.springframework.ai.openai.OpenAiChatModel;\nimport org.springframework.ai.openai.OpenAiChatOptions;\nimport org.springframework.ai.openai.OpenAiEmbeddingModel;\nimport org.springframework.ai.openai.OpenAiEmbeddingOptions;\nimport org.springframework.ai.openai.api.OpenAiApi;\nimport org.springframework.ai.zhipuai.ZhiPuAiChatModel;\nimport org.springframework.ai.zhipuai.ZhiPuAiChatOptions;\nimport org.springframework.ai.zhipuai.api.ZhiPuAiApi;\nimport org.springframework.ai.vectorstore.pgvector.PgVectorStore;\nimport org.springframework.http.client.ReactorClientHttpRequestFactory;\nimport org.springframework.jdbc.core.JdbcTemplate;\n\nimport org.springframework.http.client.reactive.ReactorClientHttpConnector;\nimport org.springframework.retry.policy.NeverRetryPolicy;\nimport org.springframework.retry.support.RetryTemplate;\nimport org.springframework.web.client.RestClient;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Flux;\nimport reactor.netty.http.client.HttpClient;\n\nimport javax.sql.DataSource;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.UUID;\n\n/**\n * 测试公共配置 — 所有测试类共享的工厂方法和常量。\n *\n * @author bigchui\n *\n */\npublic final class TestConfig {\n\n private static final Properties secrets = loadSecrets();\n\n // 便捷访问方法\n private static String s(String key) { return secrets.getProperty(key); }\n private static String s(String key, String defaultValue) { return secrets.getProperty(key, defaultValue); }\n private static int si(String key, int defaultValue) { return Integer.parseInt(s(key, String.valueOf(defaultValue))); }\n\n // ===== 公开常量(用于测试打印)=====\n public static final String CHAT_MODEL = s(\"dashscope.chat.model\", \"qwen-plus\");\n public static final String EMBEDDING_MODEL = s(\"embedding.model\", \"text-embedding-v3\");\n public static final String SKILLS_DIR = s(\"skills.dir\", \"\");\n\n /** HTTP 响应超时(5 分钟),思考模型非流式调用需要较长等待 */\n private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(300);\n\n private TestConfig() {\n }\n\n private static Properties loadSecrets() {\n Properties props = new Properties();\n try (InputStream is = TestConfig.class.getClassLoader().getResourceAsStream(\"secrets.properties\")) {\n if (is != null) {\n props.load(is);\n } else {\n System.err.println(\"[TestConfig] 未找到 secrets.properties,请复制 secrets.properties.example 并填入真实值\");\n }\n } catch (IOException e) {\n throw new RuntimeException(\"Failed to load secrets.properties\", e);\n }\n return props;\n }\n\n // ---------- Factory Methods ----------\n\n public static ChatModel createChatModel() {\n OpenAiChatOptions opts = new OpenAiChatOptions();\n opts.setModel(s(\"dashscope.chat.model\", \"qwen-plus\"));\n opts.setTemperature(0.7);\n Map<String,Object> map = new HashMap<>();\n Map<String,Object> submap = new HashMap<>();\n submap.put(\"enable_thinking\",true);\n map.put(\"chat_template_kwargs\",submap);\n opts.setExtraBody(map);\n\n HttpClient httpClient = HttpClient.create().responseTimeout(HTTP_TIMEOUT);\n\n return OpenAiChatModel.builder()\n .openAiApi(OpenAiApi.builder()\n .baseUrl(s(\"dashscope.base.url\", \"https://dashscope.aliyuncs.com/compatible-mode/\"))\n .apiKey(s(\"dashscope.api.key\"))\n .restClientBuilder(RestClient.builder()\n .requestFactory(new ReactorClientHttpRequestFactory(httpClient)))\n .webClientBuilder(WebClient.builder()\n .clientConnector(new ReactorClientHttpConnector(httpClient)))\n .build())\n .defaultOptions(opts)\n .build();\n }\n\n public static ChatModel createMiniMaxChatModel() {\n OpenAiChatOptions opts = new OpenAiChatOptions();\n opts.setModel(s(\"minimax.chat.model\", \"MiniMax-M2.7\"));\n opts.setTemperature(0.7);\n\n HttpClient httpClient = HttpClient.create().responseTimeout(HTTP_TIMEOUT);\n\n RetryTemplate noRetryTemplate = new RetryTemplate();\n noRetryTemplate.setRetryPolicy(new NeverRetryPolicy());\n\n return OpenAiChatModel.builder()\n .openAiApi(OpenAiApi.builder()\n .baseUrl(s(\"minimax.base.url\", \"https://api.minimaxi.com/\"))\n .apiKey(s(\"minimax.api.key\"))\n .restClientBuilder(RestClient.builder()\n .requestFactory(new ReactorClientHttpRequestFactory(httpClient)))\n .webClientBuilder(WebClient.builder()\n .clientConnector(new ReactorClientHttpConnector(httpClient)))\n .build())\n .defaultOptions(opts)\n .retryTemplate(noRetryTemplate)\n .build();\n }\n\n /**\n * 错误用法,deepseek v4不兼容openai chatmodel\n * @return\n */\n public static ChatModel createDeepSeekChatModel() {\n OpenAiChatOptions opts = new OpenAiChatOptions();\n opts.setModel(s(\"deepseek.chat.model\", \"deepseek-chat\"));\n opts.setTemperature(0.7);\n\n HttpClient httpClient = HttpClient.create().responseTimeout(HTTP_TIMEOUT);\n\n RetryTemplate noRetryTemplate = new RetryTemplate();\n noRetryTemplate.setRetryPolicy(new NeverRetryPolicy());\n\n return OpenAiChatModel.builder()\n .openAiApi(OpenAiApi.builder()\n .baseUrl(s(\"deepseek.base.url\", \"https://api.deepseek.com\"))\n .apiKey(s(\"deepseek.api.key\"))\n .restClientBuilder(RestClient.builder()\n .requestFactory(new ReactorClientHttpRequestFactory(httpClient)))\n .webClientBuilder(WebClient.builder()\n .clientConnector(new ReactorClientHttpConnector(httpClient)))\n .build())\n .defaultOptions(opts)\n .retryTemplate(noRetryTemplate)\n .build();\n }\n\n /**\n * 正确用法\n * @return\n */\n public static ChatModel createDeepSeekV4ChatModel(){\n DeepSeekApi deepSeekApi = DeepSeekApi.builder()\n .apiKey(s(\"deepseek.api.key\"))\n .baseUrl(s(\"deepseek.base.url\"))\n .build();\n DeepSeekChatOptions options = DeepSeekChatOptions.builder()\n .model(s(\"deepseek.chat.model\"))\n .temperature(0.7)\n .build();\n DeepSeekV4ChatModel chatModel = DeepSeekV4ChatModel.builder()\n .deepSeekApi(deepSeekApi)\n .defaultOptions(options)\n .build();\n return chatModel;\n }\n\n public static ChatModel createZhiPuChatModel() {\n ZhiPuAiApi api = ZhiPuAiApi.builder()\n .baseUrl(\"https://open.bigmodel.cn/api/coding/paas/\")\n .apiKey(s(\"zhipu.api.key\"))\n .build();\n\n ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()\n .model(s(\"zhipu.chat.model\", \"glm-4.7\"))\n .temperature(0.7)\n .thinking(ZhiPuAiApi.ChatCompletionRequest.Thinking.enabled())\n .build();\n\n // 禁用 Spring AI 内部重试,由 AgentX 框架统一处理重试\n RetryTemplate noRetryTemplate = new RetryTemplate();\n noRetryTemplate.setRetryPolicy(new NeverRetryPolicy());\n\n return new ZhiPuAiChatModel(api, options, noRetryTemplate);\n }\n\n /**\n * 创建 OpenAI 兼容风格的 ZhiPu ChatModel。\n * <p>\n * 通过 OpenAI 兼容接口访问 GLM 模型,支持 {@code reasoning_content} 字段提取。\n * <p>\n * 与 {@link #createZhiPuChatModel()} 的区别:\n * <ul>\n * <li>原生 ZhiPu 实现 — 不提取 reasoning_content,无法使用 REASONING_CONTENT 模式</li>\n * <li>OpenAI 兼容接口 — 正确提取 reasoning_content 到 metadata,支持 REASONING_CONTENT 模式</li>\n * </ul>\n */\n public static ChatModel createZhiPuOpenAiChatModel() {\n OpenAiChatOptions opts = new OpenAiChatOptions();\n opts.setModel(s(\"zhipu.chat.model\", \"glm-4.7\"));\n opts.setTemperature(0.7);\n\n // 思考模型非流式调用需要更长超时(模型生成完整响应后才开始发送)\n HttpClient httpClient = HttpClient.create()\n .responseTimeout(HTTP_TIMEOUT);\n\n RestClient.Builder restClientBuilder = RestClient.builder()\n .requestFactory(new ReactorClientHttpRequestFactory(httpClient));\n WebClient.Builder webClientBuilder = WebClient.builder()\n .clientConnector(new ReactorClientHttpConnector(httpClient));\n\n RetryTemplate noRetryTemplate = new RetryTemplate();\n noRetryTemplate.setRetryPolicy(new NeverRetryPolicy());\n\n return OpenAiChatModel.builder()\n .openAiApi(OpenAiApi.builder()\n .baseUrl(s(\"zhipu.openai.base.url\", \"https://open.bigmodel.cn/api/coding/paas/v4\"))\n .apiKey(s(\"zhipu.api.key\"))\n .completionsPath(\"/chat/completions\")\n .restClientBuilder(restClientBuilder)\n .webClientBuilder(webClientBuilder)\n .build())\n .defaultOptions(opts)\n .retryTemplate(noRetryTemplate)\n .build();\n }\n\n public static EmbeddingModel createEmbeddingModel() {\n OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()\n .model(EMBEDDING_MODEL)\n .dimensions(1024)\n .build();\n\n return new OpenAiEmbeddingModel(\n OpenAiApi.builder()\n .baseUrl(s(\"dashscope.base.url\", \"https://dashscope.aliyuncs.com/compatible-mode/\"))\n .apiKey(s(\"dashscope.api.key\"))\n .build(),\n MetadataMode.EMBED,\n options\n );\n }\n\n /**\n * 创建使用错误 API Key 的 ChatModel,用于测试 ErrorEvent。\n */\n public static ChatModel createBadApiKeyChatModel() {\n DeepSeekApi deepSeekApi = DeepSeekApi.builder()\n .apiKey(\"sk-bad-api-key-for-testing-error-event\")\n .baseUrl(s(\"deepseek.base.url\", \"https://api.deepseek.com\"))\n .build();\n DeepSeekChatOptions options = DeepSeekChatOptions.builder()\n .model(s(\"deepseek.chat.model\", \"deepseek-chat\"))\n .temperature(0.7)\n .build();\n return DeepSeekV4ChatModel.builder()\n .deepSeekApi(deepSeekApi)\n .defaultOptions(options)\n .build();\n }\n\n public static DataSource createMySqlDataSource() {\n HikariDataSource ds = new HikariDataSource();\n ds.setJdbcUrl(s(\"mysql.url\", \"jdbc:mysql://localhost:3306/agent_test?useSSL=false&allowPublicKeyRetrieval=true\"));\n ds.setUsername(s(\"mysql.user\", \"root\"));\n ds.setPassword(s(\"mysql.password\", \"root\"));\n ds.setDriverClassName(\"com.mysql.cj.jdbc.Driver\");\n return ds;\n }\n\n public static DataSource createPgDataSource() {\n HikariDataSource ds = new HikariDataSource();\n ds.setJdbcUrl(String.format(\"jdbc:postgresql://%s:%d/%s\",\n s(\"pg.host\", \"192.168.11.163\"),\n si(\"pg.port\", 5433),\n s(\"pg.database\", \"vector_store\")));\n ds.setUsername(s(\"pg.user\", \"postgres\"));\n ds.setPassword(s(\"pg.password\", \"postgres\"));\n ds.setDriverClassName(\"org.postgresql.Driver\");\n return ds;\n }\n\n public static PgVectorStore createPgVectorStore(DataSource pgDataSource, EmbeddingModel embeddingModel) {\n JdbcTemplate jdbcTemplate = new JdbcTemplate(pgDataSource);\n PgVectorStore store = PgVectorStore.builder(jdbcTemplate, embeddingModel)\n .dimensions(1024)\n .distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)\n .indexType(PgVectorStore.PgIndexType.HNSW)\n .initializeSchema(true)\n .removeExistingVectorStoreTable(false)\n .vectorTableName(s(\"pg.table.name\", \"agentx_long_term_memory\"))\n .maxDocumentBatchSize(100)\n .build();\n try {\n store.afterPropertiesSet();\n } catch (Exception e) {\n throw new RuntimeException(\"Failed to initialize PgVectorStore\", e);\n }\n return store;\n }\n\n // ---------- Helper Methods ----------\n\n public static String randomConvId() {\n return \"conv_\" + UUID.randomUUID();\n }\n\n public static String randomUserId(String prefix) {\n return prefix + \"_\" + System.currentTimeMillis();\n }\n\n public static RunnableParams buildParams(String convId, String userId) {\n return RunnableParams.builder()\n .conversationId(convId)\n .userId(userId)\n .build();\n }\n\n /**\n * 流式调用 Agent 并实时打印输出。\n */\n public static void streamAndPrint(ReactAgent agent, String query) {\n streamAndPrint(agent, query, null);\n }\n\n public static void streamAndPrint(ReactAgent agent, String query, RunnableParams params) {\n System.out.println(\"Q: \" + query);\n System.out.print(\"A: \");\n if (params != null) {\n agent.stream(query, params)\n .doOnNext(chunk -> {\n System.out.print(chunk);\n System.out.flush();\n })\n .doOnError(err -> System.err.println(\"\\nError: \" + err.getMessage()))\n .blockLast();\n } else {\n agent.stream(query)\n .doOnNext(chunk -> {\n System.out.print(chunk);\n System.out.flush();\n })\n .doOnError(err -> System.err.println(\"\\nError: \" + err.getMessage()))\n .blockLast();\n }\n System.out.println(\"\\n\");\n }\n\n public static void printTestHeader(String title) {\n System.out.println(\"\\n\" + \"=\".repeat(60));\n System.out.println(\" \" + title);\n System.out.println(\"=\".repeat(60) + \"\\n\");\n }\n\n // ---------- 流式事件工具方法 ----------\n\n /**\n * 上一个事件是否为 Thinking,用于控制标记打印\n */\n private static boolean lastEventWasThinking = false;\n\n /**\n * 当前事件来源的 Agent 名称(null = 主 Agent),用于 SubAgent 分隔\n */\n private static String currentAgentName = null;\n\n /**\n * 从事件中提取 SubAgentSource。\n */\n private static SubAgentSource extractSource(AgentStreamEvent event) {\n return switch (event) {\n case AgentStreamEvent.Text e -> e.source();\n case AgentStreamEvent.Thinking e -> e.source();\n case AgentStreamEvent.ToolStart e -> e.source();\n case AgentStreamEvent.ToolEnd e -> e.source();\n case AgentStreamEvent.Complete e -> e.source();\n default -> null;\n };\n }\n\n /**\n * 格式化打印单个流式事件。\n * <p>\n * - SubAgent 来源切换时打印 ━━━ [agent名] ━━━ 分隔线\n * - Thinking 事件只在进入思考时打印一次标记,避免每个 chunk 重复前缀\n */\n public static void printEvent(AgentStreamEvent event) {\n // SubAgent 来源切换检测\n SubAgentSource source = extractSource(event);\n String agentName = source != null ? source.agentName() : null;\n if (!java.util.Objects.equals(agentName, currentAgentName)) {\n if (currentAgentName != null) System.out.println();\n currentAgentName = agentName;\n lastEventWasThinking = false;\n String label = agentName != null ? agentName : \"main\";\n System.out.println(\"\\n━━━ [\" + label + \"] ━━━\");\n }\n\n switch (event) {\n case AgentStreamEvent.Thinking t -> {\n if (!lastEventWasThinking) {\n System.out.print(\"\\n[Thinking] \");\n }\n System.out.print(t.content());\n lastEventWasThinking = true;\n }\n case AgentStreamEvent.Text t -> {\n if (lastEventWasThinking) {\n System.out.println(\"\\n---\");\n System.out.println(\"\\n最终答案:\");\n }\n System.out.print(t.content());\n lastEventWasThinking = false;\n }\n case AgentStreamEvent.ToolStart ts ->\n System.out.println(\"\\n[ToolStart] \" + ts.toolName() + \" id=\" + ts.toolCallId());\n case AgentStreamEvent.ToolEnd te -> System.out.println(\"[ToolEnd] \" + te.toolName() + \" result=\"\n + (te.result() != null && te.result().length() > 80\n ? te.result().substring(0, 80) + \"...\"\n : te.result()));\n case AgentStreamEvent.Error e ->\n System.out.println(\"\\n[Error:\" + e.code().code() + \"] \" + e.message() + \"\\n \" + e.detail());\n case AgentStreamEvent.Complete c -> System.out.println(\"\\n[Complete]:promptTokens:\"+c.totalPromptTokens()+ \",completionTokens:\"+c.totalCompletionTokens());\n case AgentStreamEvent.Paused p -> System.out.println(\"[Paused]\");\n }\n }\n\n /**\n * 收集流式事件,实时打印所有事件,返回 PauseState(如果暂停)。\n */\n public static PauseState collectStreamEvents(Flux<AgentStreamEvent> flux) {\n PauseState[] holder = new PauseState[1];\n flux.doOnNext(event -> {\n if (event instanceof AgentStreamEvent.Paused p) {\n holder[0] = p.state();\n } else {\n printEvent(event);\n }\n }).blockLast();\n return holder[0];\n }\n}\n"}}}} |