feat: enhance local knowledge retrieval
This commit is contained in:
@@ -189,7 +189,7 @@ export type ContinueHostAdapterOptions = {
|
||||
}
|
||||
|
||||
export type ContinueHostRunOptions = {
|
||||
workMode?: 'ask' | 'plan' | 'execute'
|
||||
workMode?: 'ask' | 'execute'
|
||||
images?: AgentImage[]
|
||||
knowledgeCapability?: {
|
||||
endpoint: string
|
||||
|
||||
@@ -35,7 +35,7 @@ function createRuntime(): ContinueAgentRuntime {
|
||||
|
||||
async function collectEvents(
|
||||
runtime: ContinueAgentRuntime,
|
||||
workMode?: 'ask' | 'plan' | 'execute'
|
||||
workMode?: 'ask' | 'execute'
|
||||
): Promise<RuntimeEvent[]> {
|
||||
const events: RuntimeEvent[] = []
|
||||
for await (const event of runtime.run(
|
||||
|
||||
@@ -61,6 +61,9 @@ function settings(
|
||||
knowledgeEmbeddingBaseUrl:
|
||||
'http://127.0.0.1:11434/v1/embeddings',
|
||||
knowledgeEmbeddingModel: 'nomic-embed-text',
|
||||
knowledgeRerankEnabled: false,
|
||||
knowledgeRerankEndpoint: 'https://api.cohere.com/v1/rerank',
|
||||
knowledgeRerankModel: 'rerank-v3.5',
|
||||
workspacePath: process.cwd(),
|
||||
toolApproval: 'always',
|
||||
...overrides
|
||||
|
||||
@@ -26,11 +26,16 @@ function createService() {
|
||||
displayName: `来源 ${index}`,
|
||||
location: `/private/${index}`
|
||||
},
|
||||
chunk: { location: `第 ${index + 1} 段` },
|
||||
chunk: {
|
||||
id: `44444444-4444-4444-8444-44444444444${index}`,
|
||||
location: `第 ${index + 1} 段`
|
||||
},
|
||||
snippet: `<mark>匹配</mark> ${index}`,
|
||||
rank: index + 1,
|
||||
retrieval: {
|
||||
score: 0.5,
|
||||
channels: ['fts'] as const,
|
||||
lexicalRank: 1,
|
||||
evidenceIds: []
|
||||
}
|
||||
}
|
||||
@@ -115,9 +120,12 @@ describe('KnowledgeMcpGateway', () => {
|
||||
expect.objectContaining({
|
||||
libraryId: secondLibraryId,
|
||||
libraryName: '二号知识库',
|
||||
chunkId: '44444444-4444-4444-8444-444444444440',
|
||||
score: 0.5,
|
||||
snippet: '匹配 0'
|
||||
})
|
||||
])
|
||||
expect(references[0]?.sourceLocation).toBeUndefined()
|
||||
expect(gateway.drainReferences(token)).toEqual(references)
|
||||
expect(gateway.drainReferences(token)).toEqual([])
|
||||
await expect(
|
||||
|
||||
@@ -9,6 +9,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
||||
import { z } from 'zod'
|
||||
import type { KnowledgeSearchReference } from '../../shared/contracts'
|
||||
import { stripKnowledgeHighlightTags } from '../../shared/knowledge-text'
|
||||
import type { KnowledgeService } from '../knowledge/knowledge-service'
|
||||
import type {
|
||||
MagicNoteDetail,
|
||||
@@ -236,15 +237,12 @@ function referenceKey(reference: KnowledgeSearchReference): string {
|
||||
return [
|
||||
reference.libraryId,
|
||||
reference.documentId,
|
||||
reference.chunkId ?? '',
|
||||
reference.locator ?? '',
|
||||
reference.snippet
|
||||
].join('\0')
|
||||
}
|
||||
|
||||
function stripMarkTags(value: string): string {
|
||||
return value.replace(/<\/?mark\b[^>]*>/giu, '')
|
||||
}
|
||||
|
||||
function sendJson(
|
||||
response: ServerResponse,
|
||||
status: number,
|
||||
@@ -465,12 +463,17 @@ export class KnowledgeMcpGateway {
|
||||
libraryId: knowledgeBaseId,
|
||||
libraryName: libraryNames.get(knowledgeBaseId) ?? '知识库',
|
||||
documentId: result.document.id,
|
||||
chunkId: result.chunk.id,
|
||||
documentName: result.document.title.slice(0, 500),
|
||||
sourceName: result.source.displayName.slice(0, 500),
|
||||
sourceLocation: result.source.location?.slice(0, 4_096),
|
||||
locator: result.chunk.location?.slice(0, 1_000),
|
||||
snippet: stripMarkTags(result.snippet).slice(0, 12_000),
|
||||
snippet: stripKnowledgeHighlightTags(result.snippet).slice(0, 12_000),
|
||||
rank: result.rank,
|
||||
score: result.retrieval.score,
|
||||
lexicalRank: result.retrieval.lexicalRank,
|
||||
vectorRank: result.retrieval.vectorRank,
|
||||
graphRank: result.retrieval.graphRank,
|
||||
similarity: result.retrieval.similarity,
|
||||
retrievalChannels: result.retrieval.channels,
|
||||
evidenceIds: result.retrieval.evidenceIds?.slice(0, 100)
|
||||
}
|
||||
|
||||
@@ -453,9 +453,7 @@ describe('ModelAgentRuntime', () => {
|
||||
expect(toolProvider.listTools).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['ask', 'plan'] as const)(
|
||||
'keeps browser and workspace tools out of %s mode',
|
||||
async (workMode) => {
|
||||
it('keeps browser and workspace tools out of Ask mode', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>(async () =>
|
||||
new Response('data: {"choices":[{"delta":{"content":"只读回答"}}]}\n\ndata: [DONE]\n\n', {
|
||||
status: 200,
|
||||
@@ -475,9 +473,9 @@ describe('ModelAgentRuntime', () => {
|
||||
for await (const _event of runtime.run(
|
||||
{
|
||||
requestId: crypto.randomUUID(),
|
||||
conversationId: `conversation-${workMode}`,
|
||||
conversationId: 'conversation-ask',
|
||||
prompt: '只读',
|
||||
workMode
|
||||
workMode: 'ask'
|
||||
},
|
||||
new AbortController().signal
|
||||
)) {
|
||||
@@ -486,8 +484,7 @@ describe('ModelAgentRuntime', () => {
|
||||
|
||||
expect(toolProvider.listTools).not.toHaveBeenCalled()
|
||||
expect(toolProvider.callTool).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the OpenAI Responses endpoint and streams output text', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>(async () =>
|
||||
|
||||
@@ -461,27 +461,25 @@ describe('ModelToolProvider', () => {
|
||||
} satisfies ModelToolCallContext
|
||||
const signal = new AbortController().signal
|
||||
|
||||
for (const workMode of ['ask', 'plan'] as const) {
|
||||
const readOnlyContext = {
|
||||
conversationId: `browser-${workMode}`,
|
||||
workMode
|
||||
} satisfies ModelToolCallContext
|
||||
await expect(
|
||||
provider.listTools(readOnlyContext, signal)
|
||||
).resolves.not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'browser_screenshot' })
|
||||
])
|
||||
const readOnlyContext = {
|
||||
conversationId: 'browser-ask',
|
||||
workMode: 'ask'
|
||||
} satisfies ModelToolCallContext
|
||||
await expect(
|
||||
provider.listTools(readOnlyContext, signal)
|
||||
).resolves.not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'browser_screenshot' })
|
||||
])
|
||||
)
|
||||
await expect(
|
||||
provider.callTool(
|
||||
'browser_screenshot',
|
||||
{},
|
||||
signal,
|
||||
readOnlyContext
|
||||
)
|
||||
await expect(
|
||||
provider.callTool(
|
||||
'browser_screenshot',
|
||||
{},
|
||||
signal,
|
||||
readOnlyContext
|
||||
)
|
||||
).rejects.toThrow('未知工具')
|
||||
}
|
||||
).rejects.toThrow('未知工具')
|
||||
expect(browserService.screenshot).not.toHaveBeenCalled()
|
||||
|
||||
const tools = await provider.listTools(firstContext, signal)
|
||||
@@ -603,13 +601,6 @@ describe('ModelToolProvider', () => {
|
||||
source: 'builtin'
|
||||
})
|
||||
])
|
||||
await expect(
|
||||
provider.listTools(
|
||||
{ ...askContext, workMode: 'plan' },
|
||||
signal
|
||||
)
|
||||
).resolves.toEqual([])
|
||||
|
||||
await provider.callTool(
|
||||
'web_search',
|
||||
{ query: 'GoodBuddy current release', numResults: 3 },
|
||||
|
||||
@@ -208,7 +208,7 @@ export type ModelToolResult = {
|
||||
|
||||
export type ModelToolCallContext = {
|
||||
conversationId: string
|
||||
workMode: 'ask' | 'plan' | 'execute'
|
||||
workMode: 'ask' | 'execute'
|
||||
knowledgeCapabilityToken?: string
|
||||
}
|
||||
|
||||
@@ -1218,10 +1218,9 @@ export class ModelToolProvider implements ModelToolProviderLike {
|
||||
): Promise<ModelToolDefinition[]> {
|
||||
signal.throwIfAborted()
|
||||
const scopedTools = this.getScopedTools(context)
|
||||
const webTools =
|
||||
this.webSearchEnabled && context.workMode !== 'plan'
|
||||
? this.getWebSearchDefinitions()
|
||||
: []
|
||||
const webTools = this.webSearchEnabled
|
||||
? this.getWebSearchDefinitions()
|
||||
: []
|
||||
if (context.workMode !== 'execute') {
|
||||
return [...webTools, ...scopedTools]
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ function embeddedRuntime(
|
||||
|
||||
async function collectRun(
|
||||
runtime: OpenCodeRuntime,
|
||||
workMode: 'ask' | 'plan' | 'execute' = 'execute'
|
||||
workMode: 'ask' | 'execute' = 'execute'
|
||||
) {
|
||||
const events = []
|
||||
for await (const event of runtime.run(
|
||||
@@ -2083,9 +2083,7 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
||||
await runtime.dispose()
|
||||
})
|
||||
|
||||
it.each(['ask', 'plan'] as const)(
|
||||
'uses deny-all session rules and hard tool disable in %s mode',
|
||||
async (workMode) => {
|
||||
it('uses deny-all session rules and hard tool disable in Ask mode', async () => {
|
||||
const { client, session, tool } = runClient([
|
||||
{
|
||||
id: 'event-idle',
|
||||
@@ -2095,7 +2093,7 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
||||
])
|
||||
const runtime = embeddedRuntime(client)
|
||||
|
||||
await collectRun(runtime, workMode)
|
||||
await collectRun(runtime, 'ask')
|
||||
|
||||
expect(session.create).toHaveBeenCalledWith({
|
||||
title: 'GoodBuddy 对话',
|
||||
@@ -2119,8 +2117,7 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
||||
expect.anything()
|
||||
)
|
||||
await runtime.dispose()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('updates reused sessions when the work mode changes', async () => {
|
||||
const { client, session } = runClient([
|
||||
|
||||
@@ -160,9 +160,7 @@ describe('AgentRuntimeController', () => {
|
||||
await stream.return()
|
||||
})
|
||||
|
||||
it.each(['ask', 'plan'] as const)(
|
||||
'denies tool authorization in %s mode without prompting the user',
|
||||
async (workMode) => {
|
||||
it('denies tool authorization in Ask mode without prompting the user', async () => {
|
||||
const runtime = new TestRuntime(false, false, true)
|
||||
const controller = new AgentRuntimeController(runtime)
|
||||
const authorize = vi.fn(async () => 'once' as const)
|
||||
@@ -171,7 +169,7 @@ describe('AgentRuntimeController', () => {
|
||||
requestId: '1c608898-ecb7-4081-8174-2b6a52f53b09',
|
||||
conversationId: 'conversation-3',
|
||||
prompt: 'test',
|
||||
workMode
|
||||
workMode: 'ask'
|
||||
},
|
||||
new AbortController().signal,
|
||||
authorize
|
||||
@@ -179,8 +177,7 @@ describe('AgentRuntimeController', () => {
|
||||
|
||||
await expect(stream.next()).rejects.toThrow('tool denied')
|
||||
expect(authorize).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards per-tool authorization without adding a whole-run gate', async () => {
|
||||
const runtime = new TestRuntime(false, false, true)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type {
|
||||
AgentQuestionAnswer,
|
||||
AgentRequest,
|
||||
AgentRuntimeStatus
|
||||
} from '../../shared/contracts'
|
||||
import type {
|
||||
AgentExecutionRequest,
|
||||
AgentRuntime,
|
||||
RuntimeAuthorizer,
|
||||
RuntimeEvent
|
||||
@@ -113,7 +113,7 @@ export class AgentRuntimeController implements AgentRuntime {
|
||||
}
|
||||
|
||||
async *run(
|
||||
request: AgentRequest,
|
||||
request: AgentExecutionRequest,
|
||||
signal: AbortSignal,
|
||||
authorize?: RuntimeAuthorizer
|
||||
): AsyncGenerator<RuntimeEvent, void, void> {
|
||||
|
||||
@@ -78,6 +78,9 @@ function settings(
|
||||
knowledgeEmbeddingBaseUrl:
|
||||
'http://127.0.0.1:11434/v1/embeddings',
|
||||
knowledgeEmbeddingModel: 'embedding',
|
||||
knowledgeRerankEnabled: false,
|
||||
knowledgeRerankEndpoint: 'https://api.cohere.com/v1/rerank',
|
||||
knowledgeRerankModel: 'rerank-v3.5',
|
||||
workspacePath: process.cwd(),
|
||||
toolApproval: 'always',
|
||||
...overrides
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
AgentRequest,
|
||||
AgentRuntimeStatus
|
||||
} from '../../shared/contracts'
|
||||
import type { WorkMode } from '../../shared/assistant-contracts'
|
||||
|
||||
export type RuntimeApprovalRequest = {
|
||||
scopeKey: string
|
||||
@@ -72,7 +73,8 @@ export type AgentImage = {
|
||||
data: string
|
||||
}
|
||||
|
||||
export type AgentExecutionRequest = AgentRequest & {
|
||||
export type AgentExecutionRequest = Omit<AgentRequest, 'workMode'> & {
|
||||
workMode?: WorkMode
|
||||
images?: AgentImage[]
|
||||
/** Main-process-only instructions placed in the model system layer. */
|
||||
trustedInstructions?: string
|
||||
|
||||
Reference in New Issue
Block a user