diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 9fbc675..3779aff 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -26,6 +26,8 @@ import { memoryProfileCreate, memoryProfileGet, knowledgeRetrieve, + knowledgeSearch, + knowledgeChat, mcpCall, mcpList, mcpTools, @@ -79,6 +81,8 @@ export const commands: Record = { "memory profile create": memoryProfileCreate, "memory profile get": memoryProfileGet, "knowledge retrieve": knowledgeRetrieve, + "knowledge search": knowledgeSearch, + "knowledge chat": knowledgeChat, "mcp call": mcpCall, "mcp list": mcpList, "mcp tools": mcpTools, diff --git a/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts b/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts new file mode 100644 index 0000000..a969b40 --- /dev/null +++ b/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts @@ -0,0 +1,164 @@ +import { tmpdir } from "os"; +import { describe, expect, test } from "vite-plus/test"; +import { parseStdoutJson, runCli } from "./helpers.ts"; + +interface DryRunBody { + endpoint?: string; + request?: { + input?: { + messages?: Array<{ role: string; content: string }>; + }; + parameters?: { + agent_options?: { + agent_id?: string; + image_list?: string[]; + }; + }; + stream?: boolean; + }; +} + +describe("e2e: knowledge chat", () => { + test("knowledge chat --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["knowledge", "chat", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--message/i); + expect(stderr).toMatch(/--agent-id/i); + expect(stderr).toMatch(/--workspace-id/i); + }); + + test("缺少 --message 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "chat", + "--agent-id", + "aid_test", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--message|Usage:/i); + }); + + test("缺少 --agent-id 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "chat", + "--message", + "Hello", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--agent-id|Usage:/i); + }); + + test("缺少 --workspace-id 时非零退出并提示", async () => { + const { stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--message", + "Hello", + "--agent-id", + "aid_test", + "--non-interactive", + "--output", + "json", + ], + { + DASHSCOPE_API_KEY: "sk-fake", + BAILIAN_WORKSPACE_ID: undefined, + BAILIAN_CONFIG_DIR: tmpdir(), + }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/workspace.*required/i); + }); + + test("--dry-run 输出 endpoint 和 request body", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--message", + "什么是RAG", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/); + expect(data.endpoint).toMatch(/api\/v2\/apps\/knowledge\/chat/); + expect(data.request?.input?.messages?.[0]?.role).toBe("user"); + expect(data.request?.input?.messages?.[0]?.content).toBe("什么是RAG"); + expect(data.request?.parameters?.agent_options?.agent_id).toBe("aid_test"); + }); + + test("--dry-run 多轮消息解析 role:content 前缀", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--message", + "user:什么是RAG", + "--message", + "assistant:RAG是检索增强生成", + "--message", + "它怎么工作", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + const msgs = data.request?.input?.messages ?? []; + expect(msgs).toHaveLength(3); + expect(msgs[0]?.role).toBe("user"); + expect(msgs[0]?.content).toBe("什么是RAG"); + expect(msgs[1]?.role).toBe("assistant"); + expect(msgs[1]?.content).toBe("RAG是检索增强生成"); + expect(msgs[2]?.role).toBe("user"); + expect(msgs[2]?.content).toBe("它怎么工作"); + }); + + test("--dry-run + --image 输出 image_list", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--message", + "描述这张图", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--image", + "https://example.com/img.jpg", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.request?.parameters?.agent_options?.image_list).toEqual([ + "https://example.com/img.jpg", + ]); + }); +}); diff --git a/packages/cli/tests/e2e/knowledge-search.e2e.test.ts b/packages/cli/tests/e2e/knowledge-search.e2e.test.ts new file mode 100644 index 0000000..785f0a9 --- /dev/null +++ b/packages/cli/tests/e2e/knowledge-search.e2e.test.ts @@ -0,0 +1,180 @@ +import { tmpdir } from "os"; +import { describe, expect, test } from "vite-plus/test"; +import { parseStdoutJson, runCli } from "./helpers.ts"; + +interface DryRunBody { + endpoint?: string; + request?: { + query?: string; + agent_id?: string; + image_list?: string[]; + query_history?: Array<{ role: string; content: string }>; + }; +} + +describe("e2e: knowledge search", () => { + test("knowledge search --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["knowledge", "search", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--query/i); + expect(stderr).toMatch(/--agent-id/i); + expect(stderr).toMatch(/--workspace-id/i); + expect(stderr).toMatch(/--image/i); + expect(stderr).toMatch(/--query-history/i); + }); + + test("缺少 --query 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "search", + "--agent-id", + "aid_test", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--query|Usage:/i); + }); + + test("缺少 --agent-id 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "search", + "--query", + "test", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--agent-id|Usage:/i); + }); + + test("缺少 --workspace-id 时非零退出并提示", async () => { + const { stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--query", + "test", + "--agent-id", + "aid_test", + "--non-interactive", + "--output", + "json", + ], + { + DASHSCOPE_API_KEY: "sk-fake", + BAILIAN_WORKSPACE_ID: undefined, + BAILIAN_CONFIG_DIR: tmpdir(), + }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/workspace.*required/i); + }); + + test("--dry-run 输出 endpoint 和 request body", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "什么是RAG", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/); + expect(data.endpoint).toMatch(/api\/v1\/indices\/knowledge\/search/); + expect(data.request?.query).toBe("什么是RAG"); + expect(data.request?.agent_id).toBe("aid_test"); + }); + + test("--dry-run + --image 输出 image_list", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "test", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--image", + "https://example.com/a.jpg", + "--image", + "https://example.com/b.jpg", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.request?.image_list).toEqual([ + "https://example.com/a.jpg", + "https://example.com/b.jpg", + ]); + }); + + test("--dry-run + --query-history 输出用户对话历史", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "它怎么工作", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--query-history", + '[{"role":"user","content":"什么是RAG"},{"role":"assistant","content":"RAG是检索增强生成"}]', + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.request?.query_history).toEqual([ + { role: "user", content: "什么是RAG" }, + { role: "assistant", content: "RAG是检索增强生成" }, + ]); + }); + + test("--dry-run + --query-history 无效 JSON 非零退出", async () => { + const { stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "test", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--query-history", + "not-valid-json", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/query-history.*valid JSON/i); + }); +}); diff --git a/packages/commands/src/commands/knowledge/chat.ts b/packages/commands/src/commands/knowledge/chat.ts new file mode 100644 index 0000000..59950c9 --- /dev/null +++ b/packages/commands/src/commands/knowledge/chat.ts @@ -0,0 +1,255 @@ +import { + defineCommand, + request, + knowledgeChatEndpoint, + parseSSE, + detectOutputFormat, + BailianError, + ExitCode, + isInteractive, + type Config, + type GlobalFlags, + type KnowledgeChatRequest, + type KnowledgeChatStreamChunk, +} from "bailian-cli-core"; +import { failIfMissing, cmdUsage, emitResult, emitBare, promptText } from "bailian-cli-runtime"; + +interface ParsedMessage { + role: "user" | "assistant"; + content: string; +} + +function parseMessages(flags: GlobalFlags): ParsedMessage[] { + const messages: ParsedMessage[] = []; + if (flags.message) { + const validRoles = new Set(["user", "assistant"]); + const msgs = flags.message as string[]; + for (const m of msgs) { + const colonIdx = m.indexOf(":"); + const maybeRole = colonIdx !== -1 ? m.slice(0, colonIdx) : ""; + + if (validRoles.has(maybeRole)) { + messages.push({ role: maybeRole as "user" | "assistant", content: m.slice(colonIdx + 1) }); + } else { + messages.push({ role: "user", content: m }); + } + } + } + return messages; +} + +/** SSE step_change → human-friendly progress label (TTY only) */ +const STEP_LABELS: Record = { + tool_calling: "🔍 Retrieving...", + plan_start: "🤔 Planning...", + generation_start: "✍️ Generating...", +}; + +export default defineCommand({ + description: "Chat with a Bailian knowledge base (RAG Q&A with streaming)", + usageArgs: "--message --agent-id [flags]", + options: [ + { + flag: "--message ", + description: + "Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format", + required: true, + type: "array", + }, + { + flag: "--agent-id ", + description: "Q&A service ID (find in console knowledge Q&A page)", + required: true, + }, + { + flag: "--workspace-id ", + description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)", + }, + { + flag: "--image ", + description: "Image URL(s) (repeatable)", + type: "array", + }, + ], + notes: [ + "Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times.", + "Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.", + "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `.", + 'Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.', + ], + exampleArgs: [ + '--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx', + '--message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx', + ], + async run(config: Config, flags: GlobalFlags) { + let messages = parseMessages(flags); + + if (messages.length === 0) { + if (isInteractive({ nonInteractive: config.nonInteractive })) { + const hint = await promptText({ message: "Enter your message:" }); + if (!hint) { + process.stderr.write("Chat cancelled.\n"); + process.exit(1); + } + messages = [{ role: "user", content: hint }]; + } else { + failIfMissing("message", cmdUsage(config, "--message --agent-id ")); + } + } + + const agentId = flags.agentId as string; + if (!agentId) failIfMissing("agent-id", cmdUsage(config, "--message --agent-id ")); + + const workspaceId = (flags.workspaceId as string) || config.workspaceId; + if (!workspaceId) { + throw new BailianError( + "Workspace ID is required.", + ExitCode.USAGE, + "Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: kscli config set workspace_id ", + ); + } + + const format = detectOutputFormat(config.output); + // API only supports SSE; streamOutput controls whether to print tokens in real-time + const streamOutput = format === "text" && !!process.stdout.isTTY; + + const body: KnowledgeChatRequest = { + input: { + messages, + }, + parameters: { + agent_options: { + agent_id: agentId, + }, + }, + stream: true, + }; + + const imageUrls = flags.image as string[] | undefined; + if (imageUrls && imageUrls.length > 0) { + body.parameters.agent_options.image_list = imageUrls; + } + + const url = knowledgeChatEndpoint(workspaceId); + + if (config.dryRun) { + emitResult({ endpoint: url, request: body }, format); + return; + } + + const res = await request(config, { + url, + method: "POST", + body, + stream: true, + }); + + if (streamOutput) { + let textContent = ""; + const dim = config.noColor ? "" : "\x1b[2m"; + const reset = config.noColor ? "" : "\x1b[0m"; + const verbose = config.verbose; + + for await (const event of parseSSE(res)) { + if (event.data === "[DONE]") break; + + if (event.event === "error") { + let errMsg = "Chat API error"; + let errCode: string | undefined; + try { + const err = JSON.parse(event.data); + errMsg = err.message || errMsg; + errCode = err.code; + } catch { + /* use defaults */ + } + throw new BailianError( + errMsg, + ExitCode.GENERAL, + errCode ? `API error: ${errCode}` : undefined, + ); + } + + try { + const chunk = JSON.parse(event.data) as KnowledgeChatStreamChunk; + + for (const choice of chunk.output?.choices ?? []) { + const msg = choice.message; + + // Progress indicator (TTY text mode) + if (msg.extra?.step_change) { + const label = STEP_LABELS[msg.extra.step_change]; + if (label) { + process.stdout.write(`${dim}${label}${reset}\n`); + } + } + + // Verbose: dump all events to stderr + if (verbose && msg.extra?.step_change) { + process.stderr.write( + `${dim}[event] step_change=${msg.extra.step_change} step=${msg.extra?.step ?? ""} group=${msg.extra?.group ?? ""}${reset}\n`, + ); + } + + // Extract generated content + if (msg.content) { + textContent += msg.content; + process.stdout.write(msg.content); + } + + if (choice.finish_reason === "stop") break; + } + } catch { + // Skip unparseable chunks + } + } + + process.stdout.write("\n"); + } else { + // Buffered output: collect all chunks then emit + let textContent = ""; + let requestId = ""; + + for await (const event of parseSSE(res)) { + if (event.data === "[DONE]") break; + + if (event.event === "error") { + let errMsg = "Chat API error"; + let errCode: string | undefined; + try { + const err = JSON.parse(event.data); + errMsg = err.message || errMsg; + errCode = err.code; + } catch { + /* use defaults */ + } + throw new BailianError( + errMsg, + ExitCode.GENERAL, + errCode ? `API error: ${errCode}` : undefined, + ); + } + + try { + const chunk = JSON.parse(event.data) as KnowledgeChatStreamChunk; + if (chunk.request_id) requestId = chunk.request_id; + + for (const choice of chunk.output?.choices ?? []) { + if (choice.message?.content) { + textContent += choice.message.content; + } + if (choice.finish_reason === "stop") break; + } + } catch { + // Skip unparseable chunks + } + } + + if (config.quiet || format === "text") { + emitBare(textContent); + } else { + emitResult({ answer: textContent, request_id: requestId }, format); + } + } + }, +}); diff --git a/packages/commands/src/commands/knowledge/retrieve.ts b/packages/commands/src/commands/knowledge/retrieve.ts index aa227b2..769737d 100644 --- a/packages/commands/src/commands/knowledge/retrieve.ts +++ b/packages/commands/src/commands/knowledge/retrieve.ts @@ -23,7 +23,7 @@ import { emitResult, emitBare } from "bailian-cli-runtime"; const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com"; export default defineCommand({ - description: "Retrieve from a Bailian knowledge base", + description: "Retrieve from a Bailian knowledge base (deprecated, use `search` instead)", skipDefaultApiKeySetup: true, usageArgs: "--index-id --query [flags]", options: [ diff --git a/packages/commands/src/commands/knowledge/search.ts b/packages/commands/src/commands/knowledge/search.ts new file mode 100644 index 0000000..ae3fadf --- /dev/null +++ b/packages/commands/src/commands/knowledge/search.ts @@ -0,0 +1,139 @@ +import { + defineCommand, + requestJson, + knowledgeSearchEndpoint, + detectOutputFormat, + BailianError, + ExitCode, + isInteractive, + type Config, + type GlobalFlags, + type KnowledgeSearchRequest, + type KnowledgeSearchResponse, +} from "bailian-cli-core"; +import { failIfMissing, cmdUsage, emitResult, emitBare, promptText } from "bailian-cli-runtime"; + +export default defineCommand({ + description: "Search a Bailian knowledge base (RAG semantic retrieval)", + usageArgs: "--query --agent-id [flags]", + options: [ + { + flag: "--query ", + description: "Search query text (required, cannot be empty)", + required: true, + }, + { + flag: "--agent-id ", + description: "Retrieval service ID (find in console knowledge retrieval page)", + required: true, + }, + { + flag: "--workspace-id ", + description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)", + }, + { + flag: "--image ", + description: "Image URL for multimodal retrieval (repeatable)", + type: "array", + }, + { + flag: "--query-history ", + description: + 'User conversation history JSON for context understanding and query rewriting. Format: \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]\'', + }, + ], + notes: [ + "Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required.", + "Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.", + "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `.", + "`--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance.", + ], + exampleArgs: [ + '--query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx', + '--api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg', + '--query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]\'', + ], + async run(config: Config, flags: GlobalFlags) { + let query = flags.query as string | undefined; + if (!query) { + if (isInteractive({ nonInteractive: config.nonInteractive })) { + const hint = await promptText({ message: "Enter your search query:" }); + if (!hint) { + process.stderr.write("Search cancelled.\n"); + process.exit(1); + } + query = hint; + } else { + failIfMissing("query", cmdUsage(config, "--query --agent-id ")); + } + } + + const agentId = flags.agentId as string; + if (!agentId) failIfMissing("agent-id", cmdUsage(config, "--query --agent-id ")); + + const workspaceId = (flags.workspaceId as string) || config.workspaceId; + if (!workspaceId) { + throw new BailianError( + "Workspace ID is required.", + ExitCode.USAGE, + "Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: kscli config set workspace_id ", + ); + } + + const format = detectOutputFormat(config.output); + + const body: KnowledgeSearchRequest = { + query: query!, + agent_id: agentId, + }; + + const imageUrls = flags.image as string[] | undefined; + if (imageUrls && imageUrls.length > 0) { + body.image_list = imageUrls; + } + + // Parse query_history JSON for multi-turn context + if (flags.queryHistory) { + try { + body.query_history = JSON.parse(flags.queryHistory as string) as Array<{ + role: "user" | "assistant"; + content: string; + }>; + } catch { + throw new BailianError( + '--query-history must be valid JSON. Example: --query-history \'[{"role":"user","content":"What is RAG"}]\'', + ExitCode.USAGE, + ); + } + } + + const url = knowledgeSearchEndpoint(workspaceId); + + if (config.dryRun) { + emitResult({ endpoint: url, request: body }, format); + return; + } + + const response = await requestJson(config, { + url, + method: "POST", + body, + }); + + const nodes = response.data?.nodes || []; + if (config.quiet || format === "text") { + if (nodes.length === 0) { + emitBare("No results found."); + } else { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]!; + emitBare(`[${i + 1}] (score: ${node.score.toFixed(4)})`); + emitBare(node.text); + emitBare(""); + } + } + } else { + emitResult(response, format); + } + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 4fc1ba1..050e3ee 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -29,6 +29,8 @@ export { default as memoryDelete } from "./commands/memory/delete.ts"; export { default as memoryProfileCreate } from "./commands/memory/profile-create.ts"; export { default as memoryProfileGet } from "./commands/memory/profile-get.ts"; export { default as knowledgeRetrieve } from "./commands/knowledge/retrieve.ts"; +export { default as knowledgeSearch } from "./commands/knowledge/search.ts"; +export { default as knowledgeChat } from "./commands/knowledge/chat.ts"; export { default as mcpCall } from "./commands/mcp/call.ts"; export { default as mcpList } from "./commands/mcp/list.ts"; export { default as mcpTools } from "./commands/mcp/tools.ts"; diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 7cb4ab2..f020b2e 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -79,6 +79,18 @@ export function knowledgeRetrieveEndpoint(baseUrl: string): string { return `${baseUrl}/api/v1/indices/rag/index/retrieve`; } +// ---- Knowledge Search (新版 RAG 检索, workspace-based host) ---- + +export function knowledgeSearchEndpoint(workspaceId: string): string { + return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`; +} + +// ---- Knowledge Chat (新版 RAG 问答, workspace-based host) ---- + +export function knowledgeChatEndpoint(workspaceId: string): string { + return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat`; +} + // ---- MCP Services (Streamable HTTP) ---- export function mcpWebSearchEndpoint(baseUrl: string): string { diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 22be23e..d44942b 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -5,7 +5,9 @@ export { chatEndpoint, imageEndpoint, imageSyncEndpoint, + knowledgeChatEndpoint, knowledgeRetrieveEndpoint, + knowledgeSearchEndpoint, memoryAddEndpoint, memoryListEndpoint, memoryNodeEndpoint, diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index 87f0782..6a5ad77 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -417,6 +417,84 @@ export interface DashScopeKnowledgeRetrieveResponse { }; } +// ---- Knowledge Search (新版 RAG 检索 API, agent_id-based) ---- + +export interface KnowledgeSearchRequest { + query: string; + agent_id: string; + image_list?: string[]; + query_history?: Array<{ role: "user" | "assistant"; content: string }>; +} + +export interface KnowledgeSearchResponse { + code: string; + status_code: number; + request_id: string; + data: { + total: number; + cost_time: number; + nodes: Array<{ + score: number; + text: string; + metadata: { + content?: string; + title?: string; + doc_id?: string; + doc_name?: string; + doc_url?: string; + pipeline_id?: string; + workspace_id?: string; + page_number?: number; + image_url?: string; + _knowledge_type?: string; + _citation_index?: number; + _score?: number; + }; + }>; + }; +} + +// ---- Knowledge Chat (新版 RAG 问答 SSE API, agent_id-based) ---- + +export interface KnowledgeChatRequest { + input: { + messages: Array<{ role: "user" | "assistant"; content: string }>; + request_id?: string; + }; + parameters: { + agent_options: { + agent_id: string; + image_list?: string[]; + user?: { + user_id?: string; + workspace_id?: string; + }; + }; + }; + stream: boolean; +} + +export interface KnowledgeChatStreamChunk { + output: { + choices: Array<{ + message: { + role: string; + content: string; + tool_calls?: unknown[]; + extra?: { + group?: string; + step_change?: string; + step?: string; + }; + }; + finish_reason: string; + }>; + }; + code: string; + message: string; + request_id: string; +} + // ---- Speech Synthesis / TTS (DashScope) ---- export interface DashScopeTTSRequest { diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index fa1e40e..6495a93 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -23,8 +23,12 @@ export type { DashScopeVideoEditRequest, DashScopeVideoRefRequest, DashScopeVideoRequest, + KnowledgeChatRequest, + KnowledgeChatStreamChunk, KnowledgeRetrieveRequest, KnowledgeRetrieveResponse, + KnowledgeSearchRequest, + KnowledgeSearchResponse, MemoryAddRequest, MemoryAddResponse, MemoryMessage, diff --git a/packages/kscli/README.md b/packages/kscli/README.md index 5757458..bf44712 100644 --- a/packages/kscli/README.md +++ b/packages/kscli/README.md @@ -28,20 +28,29 @@ npm install -g knowledge-studio-cli ## Quick Start ```bash -# Retrieve from a knowledge base -kscli retrieve \ - --index-id \ - --query "What is Model Studio?" +# Search a knowledge base +kscli search \ + --query "What is Model Studio?" \ + --agent-id \ + --workspace-id + +# Chat with a knowledge base +kscli chat \ + --message "What is RAG?" \ + --agent-id \ + --workspace-id ``` ## Commands -| Command | Description | -| :------------ | :-------------------------------- | -| `retrieve` | Query a knowledge base (RAG) | -| `config show` | Display current configuration | -| `config set` | Set a configuration value | -| `update` | Self-update to the latest version | +| Command | Description | +| :------------ | :------------------------------------------------ | +| `search` | Semantic search across knowledge bases (RAG) | +| `chat` | Knowledge-base Q&A with streaming (RAG) | +| `retrieve` | Query a knowledge base (deprecated, use `search`) | +| `config show` | Display current configuration | +| `config set` | Set a configuration value | +| `update` | Self-update to the latest version | ## Authentication @@ -55,7 +64,7 @@ export DASHSCOPE_API_KEY=sk-xxxxx kscli config set --key api_key --value sk-xxxxx # Option 3: Per-command flag -kscli retrieve --api-key sk-xxxxx --index-id --query "..." +kscli search --api-key sk-xxxxx --query "..." --agent-id --workspace-id ``` ## Configuration diff --git a/packages/kscli/README.zh.md b/packages/kscli/README.zh.md index d701797..5c1334a 100644 --- a/packages/kscli/README.zh.md +++ b/packages/kscli/README.zh.md @@ -29,19 +29,28 @@ npm install -g knowledge-studio-cli ```bash # 检索知识库 -kscli retrieve \ - --index-id \ - --query "什么是 Model Studio?" +kscli search \ + --query "什么是 Model Studio?" \ + --agent-id \ + --workspace-id + +# 知识库问答 +kscli chat \ + --message "什么是RAG?" \ + --agent-id \ + --workspace-id ``` ## 命令列表 -| 命令 | 说明 | -| :------------ | :---------------- | -| `retrieve` | 查询知识库(RAG) | -| `config show` | 显示当前配置 | -| `config set` | 设置配置项 | -| `update` | 自更新到最新版本 | +| 命令 | 说明 | +| :------------ | :------------------------------------ | +| `search` | 知识库语义检索(RAG) | +| `chat` | 知识库问答(流式输出) | +| `retrieve` | 查询知识库(已弃用,请使用 `search`) | +| `config show` | 显示当前配置 | +| `config set` | 设置配置项 | +| `update` | 自更新到最新版本 | ## 认证方式 @@ -55,7 +64,7 @@ export DASHSCOPE_API_KEY=sk-xxxxx kscli config set --key api_key --value sk-xxxxx # 方式三:命令行参数 -kscli retrieve --api-key sk-xxxxx --index-id --query "..." +kscli search --api-key sk-xxxxx --query "..." --agent-id --workspace-id ``` ## 配置 diff --git a/packages/kscli/src/main.ts b/packages/kscli/src/main.ts index 1a7a37d..c99134b 100644 --- a/packages/kscli/src/main.ts +++ b/packages/kscli/src/main.ts @@ -1,6 +1,13 @@ import { createCli } from "bailian-cli-runtime"; import type { Command } from "bailian-cli-core"; -import { configShow, configSet, update, knowledgeRetrieve } from "bailian-cli-commands"; +import { + configShow, + configSet, + update, + knowledgeRetrieve, + knowledgeSearch, + knowledgeChat, +} from "bailian-cli-commands"; import pkg from "../package.json" with { type: "json" }; const commands: Record = { @@ -8,9 +15,11 @@ const commands: Record = { "config set": configSet, update, retrieve: knowledgeRetrieve, + search: knowledgeSearch, + chat: knowledgeChat, }; -createCli(commands, { +void createCli(commands, { binName: "kscli", version: pkg.version, clientName: "knowledge-studio-cli", diff --git a/packages/kscli/tests/e2e/chat.e2e.test.ts b/packages/kscli/tests/e2e/chat.e2e.test.ts new file mode 100644 index 0000000..0286ab2 --- /dev/null +++ b/packages/kscli/tests/e2e/chat.e2e.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, test } from "vite-plus/test"; +import { isChatE2EReady, parseStdoutJson, runKscli } from "./helpers.ts"; + +// ---- Types ---- + +interface ChatJsonResult { + answer: string; + request_id: string; +} + +// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ---- + +describe.skipIf(!isChatE2EReady())("e2e: kscli chat (live)", () => { + const agentId = process.env.BAILIAN_E2E_CHAT_AGENT_ID!; + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("chat (JSON mode) returns answer", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是大模型?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + expect(data.request_id).toBeTruthy(); + }); + + test("chat (text mode) returns plain text", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是RAG?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + test("chat (stream, JSON mode) collects and returns answer", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是检索增强生成?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + expect(data.request_id).toBeTruthy(); + }); + + test("chat (stream, text mode) outputs streaming text", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是向量检索?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + // Streaming text mode: output should contain some text content + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + test("chat with multi-turn messages returns context-aware answer", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "user:什么是大模型", + "--message", + "assistant:大模型是大规模语言模型,具有强大的理解和生成能力", + "--message", + "它有哪些应用场景?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + }); + + test("chat with invalid agent_id fails gracefully", async () => { + const { stderr, exitCode } = await runKscli([ + "chat", + "--message", + "test", + "--agent-id", + "aid-invalid-not-exist", + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode).not.toBe(0); + expect(stderr).toBeTruthy(); + }); +}); diff --git a/packages/kscli/tests/e2e/global-setup.ts b/packages/kscli/tests/e2e/global-setup.ts new file mode 100644 index 0000000..fc93de0 --- /dev/null +++ b/packages/kscli/tests/e2e/global-setup.ts @@ -0,0 +1,9 @@ +import { loadRootEnv } from "./helpers.ts"; + +/** + * Vitest globalSetup: load monorepo root `.env` into `process.env` before tests run. + */ +export default function vitestGlobalSetup(): () => void { + loadRootEnv(); + return () => {}; +} diff --git a/packages/kscli/tests/e2e/helpers.ts b/packages/kscli/tests/e2e/helpers.ts new file mode 100644 index 0000000..c575061 --- /dev/null +++ b/packages/kscli/tests/e2e/helpers.ts @@ -0,0 +1,129 @@ +import { execFile } from "child_process"; +import { existsSync, mkdtempSync, readFileSync } from "fs"; +import { tmpdir } from "os"; +import { promisify } from "util"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { parseEnv } from "util"; + +const execFileAsync = promisify(execFile); + +/** `packages/kscli` 根目录(含 `src/main.ts`) */ +export const kscliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +const mainTs = join(kscliPackageRoot, "src", "main.ts"); + +/** Monorepo 根(含根 `package.json` 和 `.env`) */ +export function monorepoRoot(): string { + return join(kscliPackageRoot, "..", ".."); +} + +// ---- E2E gating helpers ---- + +// ---- .env loader (cached) ---- + +let _rootEnvCache: Record | null = null; + +/** 读取 monorepo 根目录 `.env` 并缓存(.env 值优先于 shell 环境变量) */ +function getRootEnv(): Record { + if (_rootEnvCache !== null) return _rootEnvCache; + const rootEnvPath = join(monorepoRoot(), ".env"); + _rootEnvCache = existsSync(rootEnvPath) ? parseEnv(readFileSync(rootEnvPath, "utf8")) : {}; + return _rootEnvCache; +} + +/** 从 .env 或 process.env 获取值(.env 优先) */ +function envVar(key: string): string | undefined { + return getRootEnv()[key] ?? process.env[key]; +} + +// ---- E2E gating helpers ---- + +/** 显式开启后才跑真实网络 E2E */ +export function isBailianE2EEnabled(): boolean { + return envVar("BAILIAN_E2E") === "1"; +} + +/** 是否有 DashScope API Key 可用 */ +export function isDashScopeE2EReady(): boolean { + if (!isBailianE2EEnabled()) return false; + return !!envVar("DASHSCOPE_API_KEY")?.trim(); +} + +/** 知识检索 E2E 就绪:E2E 开启 + API Key + search agent ID + workspace ID */ +export function isSearchE2EReady(): boolean { + if (!isDashScopeE2EReady()) return false; + return ( + !!envVar("BAILIAN_E2E_SEARCH_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim() + ); +} + +/** 知识问答 E2E 就绪:E2E 开启 + API Key + chat agent ID + workspace ID */ +export function isChatE2EReady(): boolean { + if (!isDashScopeE2EReady()) return false; + return !!envVar("BAILIAN_E2E_CHAT_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim(); +} + +// ---- CLI runner ---- + +export interface RunCliResult { + stdout: string; + stderr: string; + exitCode: number; +} + +/** + * 子进程执行 kscli(等价于 `node packages/kscli/src/main.ts ...`)。 + */ +export async function runKscli( + args: string[], + envOverrides: NodeJS.ProcessEnv = {}, +): Promise { + try { + const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], { + cwd: kscliPackageRoot, + encoding: "utf8", + maxBuffer: 32 * 1024 * 1024, + env: { + ...process.env, + // .env values override shell env vars (ensures correct API key is used) + ...getRootEnv(), + // Unique clean config dir per run — prevents stale config.json from previous tests + BAILIAN_CONFIG_DIR: mkdtempSync(join(tmpdir(), "kscli-test-")), + NODE_NO_WARNINGS: "1", + DO_NOT_TRACK: "1", + ...envOverrides, + }, + }); + return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 }; + } catch (err: unknown) { + const e = err as { + stdout?: string; + stderr?: string; + code?: number; + }; + return { + stdout: e.stdout ?? "", + stderr: e.stderr ?? "", + exitCode: typeof e.code === "number" ? e.code : 1, + }; + } +} + +export function parseStdoutJson(stdout: string): T { + const t = stdout.trim(); + return JSON.parse(t) as T; +} + +// ---- Global setup: load root .env ---- + +/** + * Vitest globalSetup:加载 monorepo 根目录 `.env` 合并到 `process.env`。 + */ +export function loadRootEnv(): void { + const rootEnv = join(monorepoRoot(), ".env"); + if (existsSync(rootEnv)) { + const parsed = parseEnv(readFileSync(rootEnv, "utf8")); + Object.assign(process.env, parsed); + } +} diff --git a/packages/kscli/tests/e2e/search.e2e.test.ts b/packages/kscli/tests/e2e/search.e2e.test.ts new file mode 100644 index 0000000..77518c6 --- /dev/null +++ b/packages/kscli/tests/e2e/search.e2e.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, test } from "vite-plus/test"; +import { isSearchE2EReady, parseStdoutJson, runKscli } from "./helpers.ts"; + +// ---- Types ---- + +interface SearchResponse { + code: string; + status_code: number; + request_id: string; + data: { + total: number; + cost_time: number; + nodes: Array<{ + score: number; + text: string; + metadata: { + content?: string; + title?: string; + doc_id?: string; + doc_name?: string; + doc_url?: string; + pipeline_id?: string; + workspace_id?: string; + page_number?: number; + image_url?: string; + _knowledge_type?: string; + _citation_index?: number; + _score?: number; + }; + }>; + }; +} + +// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ---- + +describe.skipIf(!isSearchE2EReady())("e2e: kscli search (live)", () => { + const agentId = process.env.BAILIAN_E2E_SEARCH_AGENT_ID!; + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("search returns results in JSON mode", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "search", + "--query", + "什么是大模型", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.request_id).toBeTruthy(); + expect(data.data.total).toBeGreaterThan(0); + expect(data.data.nodes.length).toBeGreaterThan(0); + + const firstNode = data.data.nodes[0]!; + expect(typeof firstNode.score).toBe("number"); + expect(firstNode.score).toBeGreaterThanOrEqual(0); + expect(typeof firstNode.text).toBe("string"); + expect(firstNode.text.length).toBeGreaterThan(0); + }); + + test("search returns results in text mode", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "search", + "--query", + "RAG", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + // Text mode: [1] (score: 0.xxxx) followed by text content + expect(stdout).toMatch(/\[1\].*score/); + }); + + test("search with --query-history returns results", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "search", + "--query", + "它怎么工作", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--query-history", + '[{"role":"user","content":"什么是大模型"},{"role":"assistant","content":"大模型是大规模语言模型"}]', + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.data.nodes.length).toBeGreaterThan(0); + }); + + test("search with invalid agent_id fails gracefully", async () => { + const { stderr, exitCode } = await runKscli([ + "search", + "--query", + "test", + "--agent-id", + "aid-invalid-not-exist", + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode).not.toBe(0); + expect(stderr).toBeTruthy(); + }); +}); diff --git a/packages/kscli/vite.config.ts b/packages/kscli/vite.config.ts index 0499bdf..3bfbf90 100644 --- a/packages/kscli/vite.config.ts +++ b/packages/kscli/vite.config.ts @@ -1,9 +1,14 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ + test: { + globalSetup: "./tests/e2e/global-setup.ts", + testTimeout: 60_000, + hookTimeout: 60_000, + }, pack: { entry: { - rag: "src/main.ts", + kscli: "src/main.ts", }, hash: false, minify: true, diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 13478ff..d5bcf1b 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -22,7 +22,9 @@ Use this index for the full quick index and global flags. | `bl file upload` | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) | | `bl image edit` | Edit an existing image with text instructions (Qwen-Image) | [image.md](image.md) | | `bl image generate` | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) | -| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base | [knowledge.md](knowledge.md) | +| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) | [knowledge.md](knowledge.md) | +| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | [knowledge.md](knowledge.md) | +| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) | [knowledge.md](knowledge.md) | | `bl mcp call` | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) | | `bl mcp list` | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) | | `bl mcp tools` | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) | @@ -67,7 +69,7 @@ Use this index for the full quick index and global flags. | `console` | `call` | [console.md](console.md) | | `file` | `upload` | [file.md](file.md) | | `image` | `edit`, `generate` | [image.md](image.md) | -| `knowledge` | `retrieve` | [knowledge.md](knowledge.md) | +| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) | | `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) | | `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) | | `omni` | `(root)` | [omni.md](omni.md) | diff --git a/skills/bailian-cli/reference/knowledge.md b/skills/bailian-cli/reference/knowledge.md index d2a0d49..09e85d9 100644 --- a/skills/bailian-cli/reference/knowledge.md +++ b/skills/bailian-cli/reference/knowledge.md @@ -7,19 +7,55 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Description | -| ----------------------- | -------------------------------------- | -| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base | +| Command | Description | +| ----------------------- | ------------------------------------------------------------------------- | +| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) | +| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | +| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) | ## Command details +### `bl knowledge chat` + +| Field | Value | +| --------------- | ------------------------------------------------------------ | +| **Name** | `knowledge chat` | +| **Description** | Chat with a Bailian knowledge base (RAG Q&A with streaming) | +| **Usage** | `bl knowledge chat --message --agent-id [flags]` | + +#### Options + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `--message ` | array | yes | Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format | +| `--agent-id ` | string | yes | Q&A service ID (find in console knowledge Q&A page) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--image ` | array | no | Image URL(s) (repeatable) | + +#### Notes + +- Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times. +- Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page. +- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `. +- Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history. + +#### Examples + +```bash +bl knowledge chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx +``` + +```bash +bl knowledge chat --message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx +``` + ### `bl knowledge retrieve` -| Field | Value | -| --------------- | -------------------------------------------------------------- | -| **Name** | `knowledge retrieve` | -| **Description** | Retrieve from a Bailian knowledge base | -| **Usage** | `bl knowledge retrieve --index-id --query [flags]` | +| Field | Value | +| --------------- | ------------------------------------------------------------------------- | +| **Name** | `knowledge retrieve` | +| **Description** | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | +| **Usage** | `bl knowledge retrieve --index-id --query [flags]` | #### Options @@ -53,3 +89,42 @@ bl knowledge retrieve --index-id idx_xxx --query "How to use Alibaba Cloud Baili ```bash bl knowledge retrieve --api-key $DASHSCOPE_API_KEY --index-id idx_xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid ``` + +### `bl knowledge search` + +| Field | Value | +| --------------- | ------------------------------------------------------------ | +| **Name** | `knowledge search` | +| **Description** | Search a Bailian knowledge base (RAG semantic retrieval) | +| **Usage** | `bl knowledge search --query --agent-id [flags]` | + +#### Options + +| Flag | Type | Required | Description | +| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--query ` | string | yes | Search query text (required, cannot be empty) | +| `--agent-id ` | string | yes | Retrieval service ID (find in console knowledge retrieval page) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--image ` | array | no | Image URL for multimodal retrieval (repeatable) | +| `--query-history ` | string | no | User conversation history JSON for context understanding and query rewriting. Format: '[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]' | + +#### Notes + +- Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required. +- Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page. +- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `. +- `--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance. + +#### Examples + +```bash +bl knowledge search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx +``` + +```bash +bl knowledge search --api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg +``` + +```bash +bl knowledge search --query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history '[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]' +```