Skip to content

Commit bef0908

Browse files
committed
feat(knowledge): enhance initialization output with sample search results
- Added functionality to display the sample query and retrieved content after successful initialization. - Updated the CLI to include a complete response in JSON format, preserving existing result fields. - Modified the output to show matched document names and a 400-character content excerpt in both English and Chinese. - Ensured that no additional search requests are made during this process, maintaining efficiency. - Updated documentation to reflect these changes and added tests to validate the new output format.
1 parent f3f1194 commit bef0908

7 files changed

Lines changed: 165 additions & 16 deletions

File tree

‎docs/knowledge/knowledge-cli-guide.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ bl knowledge init --workspace-id <workspace-id> --dry-run
165165

166166
知识库从创建成功起持续按时计费。标准版一次性 720 小时额度由多个知识库共享,新用户开通后 30 天内有效,模型调用另计;CLI 不假定账户余额。见[计费规则](https://help.aliyun.com/zh/model-studio/billing-for-knowledge-base)。
167167

168+
初始化成功后,文本输出展示实际样例查询、命中文档名称(服务端提供时)和最多 400 字符的内容摘要。`--output json` 的 `sampleSearch.query` 是实际查询,`sampleSearch.response` 保留该次成功检索的完整响应(包括命中内容、评分和 request_id);原有 `sampleMatched` 字段保持兼容。展示复用初始化验证中的响应,不额外调用检索接口,也不将检索响应写入恢复记录。
169+
168170
保留返回的 index/agent ID、created/reused 信息和 `.bailian/knowledge/init.json`(可用 `--state-file` 指定)。重复执行先核对已有资源;同名但归属不明或请求结果不确定时不会擅自重建。创建后失败仍会输出已创建资源与计费提醒。按返回的清理动作处理本次新建资源,复用资源不属于默认清理范围;关闭终端不停止计费,成功删除知识库才能停止其规格计费。
169171

170172
---
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Init sample search output implementation plan
2+
3+
**Goal:** Display the sample query and retrieved content after successful initialization; expose the complete successful response as `sampleSearch: { query, response }` in JSON.
4+
5+
**Architecture:** Return the successful search response from the existing polling loop. Preserve existing result fields, checkpoint format, error propagation and resource handling. Text output shows the matched sample document and a bounded excerpt with English/Chinese labels. No additional search request or new flags.
6+
7+
**Tech stack:** TypeScript, existing command/runtime output helpers, Vitest.
8+
9+
User approved this design in the current task. Execute inline in the current checkout.
10+
11+
- [x] Extend workflow tests to assert response preservation, including request ID, and exactly one search per successful first/reuse run. Extend command tests for JSON and bilingual text, and runtime E2E for the serialized field. Run the targeted tests and observe missing-output failures.
12+
- [x] In `init-workflow.ts`, make `waitForSample` return a successful checked value. Return `sampleSearch` alongside `sampleMatched`. In `init.ts`, render the query, document name and a 400-character content excerpt from a node matching the sample; keep full JSON response unchanged.
13+
- [x] Document output fields in `docs/knowledge/knowledge-cli-guide.md`. Run targeted init tests and `pnpm exec vp check`, inspect the diff and record results. No new cloud resources are needed for this output-only change; historical live transcripts remain unchanged.
14+
15+
Validation: the initial targeted run failed on four missing-output assertions; implementation then passed 69 tests across seven init test files. `vp check --fix` passed with three unrelated pre-existing warnings. Skill assets were regenerated without changes. No new live resources were created.

‎packages/commands/src/commands/knowledge/init-workflow.ts‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,14 @@ export async function runKnowledgeInit(options: InitRunOptions) {
220220
agent_version: "beta",
221221
agent_config: completed.config,
222222
});
223-
await waitForSample(
224-
async () =>
225-
matchesInitSample(
226-
await searchKnowledge(client, workspaceId, {
227-
agent_id: state.agentId!,
228-
agent_version: "beta",
229-
query: INIT_SAMPLE.query,
230-
}),
231-
),
232-
options,
233-
);
223+
const sampleResponse = await waitForSample(async () => {
224+
const response = await searchKnowledge(client, workspaceId, {
225+
agent_id: state.agentId!,
226+
agent_version: "beta",
227+
query: INIT_SAMPLE.query,
228+
});
229+
return matchesInitSample(response) ? response : false;
230+
}, options);
234231
state.phase = "verified";
235232
await checkpoint();
236233
return {
@@ -239,20 +236,22 @@ export async function runKnowledgeInit(options: InitRunOptions) {
239236
fileId: state.fileId,
240237
agentVersion: "beta" as const,
241238
sampleMatched: true as const,
239+
sampleSearch: { query: INIT_SAMPLE.query, response: sampleResponse },
242240
resources,
243241
stateFile,
244242
timeToFirstValueMs: Date.now() - startedAt,
245243
};
246244
});
247245
}
248246

249-
async function waitForSample(
250-
check: () => Promise<boolean>,
247+
async function waitForSample<Result>(
248+
check: () => Promise<Result | false>,
251249
options: InitRunOptions,
252-
): Promise<void> {
250+
): Promise<Result> {
253251
const deadline = Date.now() + options.settings.timeout * 1000;
254252
while (Date.now() < deadline) {
255-
if (await check()) return;
253+
const result = await check();
254+
if (result) return result;
256255
const remaining = deadline - Date.now();
257256
if (remaining <= 0) break;
258257
await new Promise((resolve) =>

‎packages/commands/src/commands/knowledge/init.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { emitBare, emitResult } from "bailian-cli-runtime";
1111
import { knowledgeCreationRisk, localizedKnowledgeBillingNotice } from "./billing.ts";
1212
import { prepareKnowledgeInit, type InitPrepared } from "./init-prepare.ts";
1313
import { runKnowledgeInit, type InitResource } from "./init-workflow.ts";
14+
import { matchesInitSample } from "./init-sample.ts";
1415
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
1516
import deleteIndex from "./kb-delete.ts";
1617
import deleteService from "./service-delete.ts";
@@ -214,6 +215,26 @@ export default defineCommand<typeof INIT_FLAGS, InitPrepared>({
214215
emitBare(
215216
`indexId: ${result.indexId}\nagentId: ${result.agentId}\nstateFile: ${result.stateFile}`,
216217
);
218+
const sampleNode = result.sampleSearch.response.data.nodes.find((node) =>
219+
matchesInitSample({ data: { nodes: [node] } }),
220+
);
221+
const content = sampleNode?.metadata?.content || sampleNode?.text || "";
222+
const excerpt = content.length > 400 ? `${content.slice(0, 400)}…` : content;
223+
emitBare(context.localize({ "en-US": "Sample search", "zh-CN": "样例检索" }));
224+
emitBare(
225+
context.localize({
226+
"en-US": `Query: ${result.sampleSearch.query}`,
227+
"zh-CN": `查询:${result.sampleSearch.query}`,
228+
}),
229+
);
230+
if (sampleNode?.metadata?.doc_name)
231+
emitBare(
232+
context.localize({
233+
"en-US": `Document: ${sampleNode.metadata.doc_name}`,
234+
"zh-CN": `文档:${sampleNode.metadata.doc_name}`,
235+
}),
236+
);
237+
emitBare(context.localize({ "en-US": `Content: ${excerpt}`, "zh-CN": `内容:${excerpt}` }));
217238
if (nextSearch) emitBare(renderAction(context.identity.binName, nextSearch));
218239
emitBare(notices[0]!.message);
219240
if (cleanup.length)

‎packages/commands/tests/e2e/knowledge/knowledge-init-runtime.e2e.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ test("confirmed init uses the full runtime and reuses the same resources on a se
8888
indexId: "index-test",
8989
agentId: "agent-test",
9090
sampleMatched: true,
91+
sampleSearch: {
92+
response: { data: { nodes: expect.any(Array) } },
93+
query: expect.stringContaining("RAG"),
94+
},
9195
});
9296
expect(payload.cleanup).toContainEqual({
9397
path: ["knowledge", "delete"],

‎packages/commands/tests/knowledge/knowledge-init-command.test.ts‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,22 @@ import deleteIndex from "../../src/commands/knowledge/kb-delete.ts";
55
import deleteService from "../../src/commands/knowledge/service-delete.ts";
66
import deleteFile from "../../src/commands/knowledge/file-delete.ts";
77
import search from "../../src/commands/knowledge/search.ts";
8+
import { INIT_SAMPLE } from "../../src/commands/knowledge/init-sample.ts";
89
import * as workflow from "../../src/commands/knowledge/init-workflow.ts";
910

11+
const sampleSearch = {
12+
query: INIT_SAMPLE.query,
13+
response: {
14+
code: "Success",
15+
status_code: 200,
16+
request_id: "search-request-1",
17+
data: {
18+
total: 1,
19+
cost_time: 12,
20+
nodes: [{ text: INIT_SAMPLE.content, score: 0.9, metadata: { doc_name: "sample-document" } }],
21+
},
22+
},
23+
};
1024
afterEach(() => vi.restoreAllMocks());
1125
function fixture(binName = "kscli") {
1226
const registry = new CommandRegistry(
@@ -53,12 +67,14 @@ test.each(["kscli", "bl"])(
5367
fileId: "file-1",
5468
agentVersion: "beta",
5569
sampleMatched: true,
70+
sampleSearch,
5671
resources: [...resources],
5772
stateFile: "state.json",
5873
timeToFirstValueMs: 1,
5974
});
6075
await command.run(context);
6176
const output = JSON.parse(stdout.mock.calls.map(([text]) => text).join(""));
77+
expect(output.sampleSearch).toEqual(sampleSearch);
6278
expect(output.cleanup).toEqual([
6379
{
6480
path: binName === "kscli" ? ["kb", "delete"] : ["knowledge", "delete"],
@@ -102,6 +118,7 @@ test("missing product route omits the command instead of inventing one", async (
102118
fileId: "file-1",
103119
agentVersion: "beta",
104120
sampleMatched: true,
121+
sampleSearch,
105122
resources: [{ kind: "knowledge-base", id: "index-1", created: true }],
106123
stateFile: "state.json",
107124
timeToFirstValueMs: 1,
@@ -123,3 +140,63 @@ test.each([
123140
])("invalid initialization flags fail before preparation %#", (flags) => {
124141
expect(command.validate?.(flags)).toBeDefined();
125142
});
143+
144+
test.each(["en-US", "zh-CN"] as const)(
145+
"text output shows actual sample query and retrieved content in %s",
146+
async (language) => {
147+
const { context, stdout } = fixture();
148+
context.settings.output = "text";
149+
context.settings.quiet = false;
150+
context.localize = (text) => (typeof text === "string" ? text : text[language]);
151+
vi.spyOn(workflow, "runKnowledgeInit").mockResolvedValue({
152+
indexId: "index-1",
153+
agentId: "agent-1",
154+
fileId: "file-1",
155+
agentVersion: "beta",
156+
sampleMatched: true,
157+
sampleSearch,
158+
resources: [],
159+
stateFile: "state.json",
160+
timeToFirstValueMs: 1,
161+
});
162+
await command.run(context);
163+
const output = stdout.mock.calls.map(([text]) => text).join("");
164+
expect(output).toContain(INIT_SAMPLE.query);
165+
expect(output).toContain("sample-document");
166+
expect(output).toContain("RAG means retrieval-augmented generation.");
167+
expect(output).toContain(language === "zh-CN" ? "样例检索" : "Sample search");
168+
},
169+
);
170+
171+
test("text summary selects the matched node, prefers metadata content and bounds the excerpt", async () => {
172+
const { context, stdout } = fixture();
173+
context.settings.output = "text";
174+
const content = "BAILIAN_CLI_INIT_SAMPLE_V1 " + "retrieved content ".repeat(40);
175+
const response = {
176+
...sampleSearch.response,
177+
data: {
178+
...sampleSearch.response.data,
179+
nodes: [
180+
{ text: "Unrelated result", score: 1, metadata: { doc_name: "unrelated" } },
181+
{ text: "Alternative representation", score: 0.9, metadata: { content } },
182+
],
183+
},
184+
};
185+
vi.spyOn(workflow, "runKnowledgeInit").mockResolvedValue({
186+
indexId: "index-1",
187+
agentId: "agent-1",
188+
fileId: "file-1",
189+
agentVersion: "beta",
190+
sampleMatched: true,
191+
sampleSearch: { query: INIT_SAMPLE.query, response },
192+
resources: [],
193+
stateFile: "state.json",
194+
timeToFirstValueMs: 1,
195+
});
196+
await command.run(context);
197+
const output = stdout.mock.calls.map(([text]) => text).join("");
198+
expect(output).toContain(`Content: ${content.slice(0, 400)}…`);
199+
expect(output).not.toContain(content);
200+
expect(output).not.toContain("Unrelated result");
201+
expect(output).not.toContain("Document: undefined");
202+
});

‎packages/commands/tests/knowledge/knowledge-init-workflow.test.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ async function fixture() {
4242
serviceFailure: undefined as Error | undefined,
4343
searchFailure: undefined as Error | undefined,
4444
emptySearch: false,
45+
emptySearchesRemaining: 0,
46+
searchCount: 0,
4547
unknownIndexResult: false,
4648
};
4749
const calls: string[] = [];
@@ -125,7 +127,11 @@ async function fixture() {
125127
if (body.query !== undefined) {
126128
if (remote.searchFailure) throw remote.searchFailure;
127129
expect(body.agent_version).toBe("beta");
130+
remote.searchCount += 1;
131+
if (remote.emptySearchesRemaining-- > 0)
132+
return { request_id: "not-ready", data: { nodes: [] } };
128133
return {
134+
request_id: "search-request-1",
129135
data: { nodes: remote.emptySearch ? [] : [{ text: INIT_SAMPLE.content, score: 0.9 }] },
130136
};
131137
}
@@ -153,14 +159,21 @@ async function fixture() {
153159
}
154160

155161
test("first execution creates and verifies resources; second execution performs no creation or update", async () => {
156-
const { execute, calls, options, report } = await fixture();
162+
const { execute, calls, options, report, requestJson } = await fixture();
157163
const first = await execute();
158164
expect(first).toMatchObject({
159165
indexId: "index-1",
160166
fileId: "file-1",
161167
agentId: "agent-1",
162168
agentVersion: "beta",
163169
sampleMatched: true,
170+
sampleSearch: {
171+
query: INIT_SAMPLE.query,
172+
response: {
173+
request_id: "search-request-1",
174+
data: { nodes: [{ text: INIT_SAMPLE.content, score: 0.9 }] },
175+
},
176+
},
164177
});
165178
expect(await stateStore.readStateFile(options.stateFile, localize)).toMatchObject({
166179
phase: "verified",
@@ -171,12 +184,18 @@ test("first execution creates and verifies resources; second execution performs
171184
expect(report).toHaveBeenCalledWith(
172185
expect.objectContaining({ kind: "knowledge-base", id: "index-1", created: true }),
173186
);
187+
expect(
188+
requestJson.mock.calls.filter(([request]) => request.body?.query !== undefined),
189+
).toHaveLength(1);
174190
const previousLength = calls.length;
175191
expect(await execute()).toMatchObject({
176192
indexId: first.indexId,
177193
agentId: first.agentId,
178194
sampleMatched: true,
179195
});
196+
expect(
197+
requestJson.mock.calls.filter(([request]) => request.body?.query !== undefined),
198+
).toHaveLength(2);
180199
expect(calls.slice(previousLength)).not.toEqual(
181200
expect.arrayContaining([RAG_PATHS.indexCreateV2]),
182201
);
@@ -284,3 +303,15 @@ test("state changes between prepare and execution stop before any cloud mutation
284303
).rejects.toThrow(/changed/);
285304
expect(requestJson).not.toHaveBeenCalled();
286305
});
306+
307+
test("returns the successful polling response without another search and does not persist it", async () => {
308+
const { execute, remote, options } = await fixture();
309+
remote.emptySearchesRemaining = 1;
310+
const result = await execute();
311+
expect(result.sampleSearch.response.request_id).toBe("search-request-1");
312+
expect(result.sampleSearch.response.data.nodes[0].text).toBe(INIT_SAMPLE.content);
313+
expect(remote.searchCount).toBe(2);
314+
expect(await stateStore.readStateFile(options.stateFile, localize)).not.toHaveProperty(
315+
"sampleSearch",
316+
);
317+
});

0 commit comments

Comments
 (0)