diff --git a/packages/tool-bailian-kb/README.md b/packages/tool-bailian-kb/README.md index 16f9f7f..965a076 100644 --- a/packages/tool-bailian-kb/README.md +++ b/packages/tool-bailian-kb/README.md @@ -22,8 +22,8 @@ |---|---|---|---|---| | `DASHSCOPE_API_KEY` | —(无 settings 面) | —(无 config 面) | ✅ | 工具调用报错并引导配置 | | `BAILIAN_WORKSPACE_ID` | ✅ `workspaceId` | ✅ `workspaceId` | ✅ | 工具调用报错并引导配置 | -| `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | ✅ | `kb_search` 的 `agent_id` 参数变必填 | -| `BAILIAN_DEFAULT_CHAT_AGENT_ID` | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | ✅ | `kb_chat` 的 `agent_id` 参数变必填 | +| `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | ✅ | `kb_search` 无 `agent_id` 的**程序化**调用报错并附配置指引(模型侧 schema 恒必填) | +| `BAILIAN_DEFAULT_CHAT_AGENT_ID` | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | ✅ | `kb_chat` 无 `agent_id` 的**程序化**调用报错并附配置指引(模型侧 schema 恒必填) | 行为参数(`endpointHost`/`agentVersion`/`chatTimeoutMs`)在 config/settings 层(设置文档可改,实时生效)。 @@ -59,19 +59,19 @@ Config 同时注册为 `bailian-kb` settings namespace(`installSettingsSection |---|---|---|---| | `workspaceId` | string? | — | 百炼工作空间 id;API host 为 workspace 子域名 `https://.`。未设置时每次调用回退 `BAILIAN_WORKSPACE_ID` credential | | `endpointHost` | string | `cn-beijing.maas.aliyuncs.com` | host 后缀,其他 region/私有化时替换 | -| `defaultRetrieveAgentId` | string? | — | 默认检索服务;`kb_search` 的 `agent_id` 参数 schema 恒可选,默认值每次调用运行时解析(settings/config → credential) | -| `defaultChatAgentId` | string? | — | 默认对话服务;`kb_chat` 的 `agent_id` 参数 schema 恒可选,默认值每次调用运行时解析(settings/config → credential) | +| `defaultRetrieveAgentId` | string? | — | 默认检索服务;`kb_search` 的 `agent_id` 参数 schema **恒必填**(模型永远显式传),此默认仅作用于省略 `agent_id` 的程序化调用,每次调用运行时解析(settings/config → credential) | +| `defaultChatAgentId` | string? | — | 默认对话服务;`kb_chat` 的 `agent_id` 参数 schema **恒必填**(模型永远显式传),此默认仅作用于省略 `agent_id` 的程序化调用,每次调用运行时解析(settings/config → credential) | | `agentVersion` | string? | — | `beta`(草稿调试)或已发布版本号;不暴露给模型 | | `chatTimeoutMs` | number | 300000 | kb_chat 超时;服务端是分钟级 agentic loop | -凭证与回退链:`DASHSCOPE_API_KEY` 只走 `ctx.credentials` 引用(write-only,每次调用重新解析,热更换生效);`workspaceId`/`defaultRetrieveAgentId`/`defaultChatAgentId` 先取 settings 解析值(用户层 > entry config),缺失时回退同名 credential(`BAILIAN_WORKSPACE_ID`/`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`/`BAILIAN_DEFAULT_CHAT_AGENT_ID`),都没有时报错并附配置指引。 +凭证与回退链:`DASHSCOPE_API_KEY` 只走 `ctx.credentials` 引用(write-only,每次调用重新解析,热更换生效);`workspaceId`/`defaultRetrieveAgentId`/`defaultChatAgentId` 先取 settings 解析值(用户层 > entry config),缺失时回退同名 credential(`BAILIAN_WORKSPACE_ID`/`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`/`BAILIAN_DEFAULT_CHAT_AGENT_ID`),都没有时报错并附配置指引。注意:`agent_id` 在两个工具的 schema 中恒必填,模型路径不会触发默认服务回退;回退保留是为程序化调用与 credential 热切换。 ## 工具 | 工具 | 参数 | 返回 | |---|---|---| -| `kb_search` | `query`、`agent_id`(见 defaultRetrieveAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total | -| `kb_chat` | `message`、`agent_id`(见 defaultChatAgentId) | 完整答案(内部消费 SSE 流缓冲返回)+ request_id | +| `kb_search` | `query`、`agent_id`(**必填**;程序化省略时回退 defaultRetrieveAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total | +| `kb_chat` | `message`、`agent_id`(**必填**;程序化省略时回退 defaultChatAgentId) | 完整答案(内部消费 SSE 流缓冲返回)+ request_id | 服务发现(`kb_service_list` 已移除):通过 `kscli service list` CLI 命令查询可用检索/对话服务及其 agent_id。 diff --git a/packages/tool-bailian-kb/src/skill.ts b/packages/tool-bailian-kb/src/skill.ts index af080a5..60b9110 100644 --- a/packages/tool-bailian-kb/src/skill.ts +++ b/packages/tool-bailian-kb/src/skill.ts @@ -21,7 +21,9 @@ export function registerSkill(ctx: Context): void { name: 'bailian-kb-management', description: 'Manage Bailian knowledge bases with the kscli CLI: create/update KBs, upload documents, deploy ' - + 'retrieval services, and maintain chunks. Retrieval itself uses the native kb_search/kb_chat tools.', + + 'retrieval services, and maintain chunks. Retrieval itself uses the native kb_search/kb_chat tools. ' + + 'Credentials and workspace for kb_search/kb_chat resolve automatically from DSH config ' + + '(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml).', content, source: 'bundled', resourceBase: { kind: 'directory', path: SKILL_DIR }, diff --git a/packages/tool-bailian-kb/src/tools.ts b/packages/tool-bailian-kb/src/tools.ts index 9192af6..0db5d6e 100644 --- a/packages/tool-bailian-kb/src/tools.ts +++ b/packages/tool-bailian-kb/src/tools.ts @@ -1,9 +1,10 @@ /** - * The two model-facing knowledge tools (kb_search, kb_chat). agent_id stays optional in the schema - * regardless of deployment: the default services (patch config or credential) - * can change at runtime through the credentials domain, so the fallback runs - * per call and a missing default surfaces as an executable error instead of a - * load-time schema difference. + * The two model-facing knowledge tools (kb_search, kb_chat). agent_id is REQUIRED in the schema: + * a model cannot know from the tool spec whether this deployment configures a default service, and a + * missing default previously only surfaced at call time, forcing a wasted round-trip. The per-call + * fallback to a configured default (settings/config or credential) is retained as defense-in-depth, + * but note defineTool validates args against the schema before execute, so through that entry point + * the fallback is inert; the model-facing contract is explicit. */ import { defineTool } from '@deepseek-ai/dsh-tools' @@ -45,13 +46,22 @@ export function createKbTools(deps: KbToolDeps) { const { client, resolveDefaultRetrieveAgentId, resolveDefaultChatAgentId } = deps const agentIdParam = { type: 'string' as const, - description: 'Retrieval/Q&A service id; omit to use the default service when this deployment configures one (find ids via `kscli service list`).', + required: true as const, + description: 'Retrieval/Q&A service id. REQUIRED: the schema cannot know whether this deployment ' + + 'configures a default service, so always pass one. Find ids via ' + + '`kscli service list --scene search --workspace-id ` (workspaceId resolves ' + + 'automatically from DSH settings: bailian-kb.workspaceId in ~/.dsh/settings.yaml).', } const resolveRetrieveAgentId = async (supplied: string | undefined): Promise => { if (supplied !== undefined) return supplied const defaultId = resolveDefaultRetrieveAgentId === undefined ? undefined : await resolveDefaultRetrieveAgentId() if (defaultId === undefined) { - throw new Error('agent_id is required: no default retrieval service is configured; discover services with `kscli service list`') + throw new Error( + 'agent_id is required: no default retrieval service is configured. Pass agent_id explicitly ' + + '(find ids: `kscli service list --scene search --workspace-id `), or configure a ' + + 'default: bailian-kb.defaultRetrieveAgentId in ~/.dsh/settings.yaml or ' + + 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID in ~/.dsh/.credentials.yaml.', + ) } return defaultId } @@ -59,7 +69,12 @@ export function createKbTools(deps: KbToolDeps) { if (supplied !== undefined) return supplied const defaultId = resolveDefaultChatAgentId === undefined ? undefined : await resolveDefaultChatAgentId() if (defaultId === undefined) { - throw new Error('agent_id is required: no default chat service is configured; discover services with `kscli service list`') + throw new Error( + 'agent_id is required: no default chat service is configured. Pass agent_id explicitly ' + + '(find ids: `kscli service list --scene chat --workspace-id `), or configure a ' + + 'default: bailian-kb.defaultChatAgentId in ~/.dsh/settings.yaml or ' + + 'BAILIAN_DEFAULT_CHAT_AGENT_ID in ~/.dsh/.credentials.yaml.', + ) } return defaultId } @@ -71,7 +86,10 @@ export function createKbTools(deps: KbToolDeps) { + 'references for you to verify, cite, or combine with other context. Retrieval scope and strategy ' + '(multi-KB weighting, routing, reranking) come from the service configuration. ' + 'top_k caps how many chunks return (client-side cut of the score-ranked results). ' - + 'Use kb_chat instead when the user question can be answered by the knowledge base alone.', + + 'Use kb_chat instead when the user question can be answered by the knowledge base alone. ' + + 'Credentials and workspace resolve automatically from DSH config ' + + '(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml) — ' + + 'never read or pass them yourself. agent_id is REQUIRED (see its parameter description).', parameters: { query: { type: 'string', required: true, description: 'Search query text.' }, agent_id: agentIdParam, @@ -139,7 +157,10 @@ export function createKbTools(deps: KbToolDeps) { + '(multi-round retrieval + reranking + grounded generation). For knowledge Q&A this typically outperforms ' + 'searching and synthesizing yourself when the question can be answered by the knowledge base alone; ' + 'use kb_search instead when you need raw chunks to verify, cite, or combine with other work. ' - + 'The pipeline runs an internal analysis/retrieval loop and may take a few minutes.', + + 'The pipeline runs an internal analysis/retrieval loop and may take a few minutes. ' + + 'Credentials and workspace resolve automatically from DSH config ' + + '(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml) — ' + + 'never read or pass them yourself. agent_id is REQUIRED (see its parameter description).', parameters: { message: { type: 'string', required: true, description: 'The question to ask.' }, agent_id: agentIdParam, diff --git a/packages/tool-bailian-kb/tests/tools.test.ts b/packages/tool-bailian-kb/tests/tools.test.ts index 60621a9..db5c35e 100644 --- a/packages/tool-bailian-kb/tests/tools.test.ts +++ b/packages/tool-bailian-kb/tests/tools.test.ts @@ -36,44 +36,41 @@ describe('createKbTools', () => { expect(body.agent_id).toBe('aid-1') }) - it('agent_id stays optional in the schema regardless of a configured default', () => { - const withoutDefault = toolsWith(vi.fn()).byName.kb_search! - const withDefault = toolsWith(vi.fn(), undefined, async () => 'aid-fixed').byName.kb_search! + it('agent_id is required in the schema for both tools', () => { + const { byName } = toolsWith(vi.fn()) // defineTool compiles the spec into JSON Schema: requiredness lives in the top-level `required` array. const requiredList = (tool: { parameters: Record }) => (tool.parameters.required ?? []) as string[] - // The default can arrive or leave at runtime via the credentials domain, so - // the schema cannot promise requiredness either way. - expect(requiredList(withoutDefault)).not.toContain('agent_id') - expect(requiredList(withDefault)).not.toContain('agent_id') + expect(requiredList(byName.kb_search!)).toContain('agent_id') + expect(requiredList(byName.kb_chat!)).toContain('agent_id') }) - it('kb_search falls back to the per-call default retrieve resolver as an explicit resolve step', async () => { + it('a missing agent_id is rejected by schema validation before execute (even with a default resolver)', async () => { const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) - const { byName } = toolsWith(postJson, undefined, async () => 'aid-fixed') - await byName.kb_search!.execute({ query: 'q' }, EXEC) - expect((postJson.mock.calls[0]![1] as Record).agent_id).toBe('aid-fixed') - }) - - it('a missing agent_id without any default resolves to executable discovery guidance', async () => { - const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) - const { byName } = toolsWith(postJson) - const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e) - expect((err as Error).message).toContain('kscli service list') - }) - - it('kb_search re-resolves the default per call (credential hot-swap contract)', async () => { - const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) - let current: string | undefined - const resolveDefaultRetrieveAgentId = vi.fn(async () => current) + const resolveDefaultRetrieveAgentId = vi.fn(async () => 'aid-fixed') const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId) - current = 'aid-one' - await byName.kb_search!.execute({ query: 'q' }, EXEC) - current = undefined - await byName.kb_search!.execute({ query: 'q' }, EXEC).catch(() => {}) - expect(resolveDefaultRetrieveAgentId).toHaveBeenCalledTimes(2) - expect((postJson.mock.calls[0]![1] as Record).agent_id).toBe('aid-one') - expect(postJson).toHaveBeenCalledTimes(1) + // defineTool validates args against the compiled schema before execute runs, + // so with agent_id required the per-call default fallback is never consulted + // through this entry point; it stays as defense-in-depth only. + const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e) + expect((err as Error).message).toContain('agent_id') + expect(resolveDefaultRetrieveAgentId).not.toHaveBeenCalled() + expect(postJson).not.toHaveBeenCalled() + }) + + it('a missing agent_id without any default is also a schema rejection, not the runtime guidance error', async () => { + const { byName } = toolsWith(vi.fn()) + const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e) + expect((err as Error).message).toContain('missing required property') + }) + + it('a supplied agent_id bypasses the default resolver entirely', async () => { + const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) + const resolveDefaultRetrieveAgentId = vi.fn(async () => 'aid-default') + const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId) + await byName.kb_search!.execute({ query: 'q', agent_id: 'aid-explicit' }, EXEC) + expect(resolveDefaultRetrieveAgentId).not.toHaveBeenCalled() + expect((postJson.mock.calls[0]![1] as Record).agent_id).toBe('aid-explicit') }) it('a 4xx failure passes the original error through unchanged', async () => {