mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
chore: webSearch & FetchUrl Sync #1260
This commit is contained in:
parent
1be4e0b100
commit
6bdad1328e
6 changed files with 45 additions and 49 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -19,6 +19,7 @@ plugins/cdn/
|
|||
.worktrees/
|
||||
.kimi-code/local.toml
|
||||
.kimi-sandbox/
|
||||
.vscode/
|
||||
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
|
|
|
|||
|
|
@ -48,18 +48,11 @@ export class MoonshotWebSearchProvider implements WebSearchProvider {
|
|||
async search(
|
||||
query: string,
|
||||
options?: {
|
||||
limit?: number;
|
||||
includeContent?: boolean;
|
||||
toolCallId?: string;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Promise<WebSearchResult[]> {
|
||||
const body = {
|
||||
text_query: query,
|
||||
limit: options?.limit ?? 5,
|
||||
enable_page_crawling: options?.includeContent ?? false,
|
||||
timeout_seconds: 30,
|
||||
};
|
||||
const body = { text_query: query };
|
||||
const bodyJson = JSON.stringify(body);
|
||||
|
||||
const toolCallId = options?.toolCallId;
|
||||
|
|
@ -90,7 +83,6 @@ export class MoonshotWebSearchProvider implements WebSearchProvider {
|
|||
};
|
||||
if (typeof r.date === 'string' && r.date.length > 0) out.date = r.date;
|
||||
if (typeof r.site_name === 'string' && r.site_name.length > 0) out.siteName = r.site_name;
|
||||
if (typeof r.content === 'string' && r.content.length > 0) out.content = r.content;
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,12 @@ export interface WebSearchResult {
|
|||
snippet: string;
|
||||
date?: string;
|
||||
siteName?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface WebSearchProvider {
|
||||
search(
|
||||
query: string,
|
||||
options?: {
|
||||
limit?: number;
|
||||
includeContent?: boolean;
|
||||
toolCallId?: string;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
|
|
@ -55,23 +52,6 @@ export interface WebSearchProvider {
|
|||
|
||||
export const WebSearchInputSchema = z.object({
|
||||
query: z.string().describe('The query text to search for.'),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(20)
|
||||
.default(5)
|
||||
.describe(
|
||||
'The number of results to return. Typically you do not need to set this value. When the results do not contain what you need, you probably want to give a more concrete query.',
|
||||
)
|
||||
.optional(),
|
||||
include_content: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe(
|
||||
'Whether to include the content of the web pages in the results. It can consume a large amount of tokens when this is set to true. You should avoid enabling this when `limit` is set to a large value.',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type WebSearchInput = z.infer<typeof WebSearchInputSchema>;
|
||||
|
|
@ -102,18 +82,7 @@ export class WebSearchTool implements BuiltinTool<WebSearchInput> {
|
|||
{ toolCallId, signal }: ExecutableToolContext,
|
||||
): Promise<ExecutableToolResult> {
|
||||
try {
|
||||
const opts: {
|
||||
limit?: number;
|
||||
includeContent?: boolean;
|
||||
toolCallId?: string;
|
||||
signal?: AbortSignal;
|
||||
} = {
|
||||
toolCallId,
|
||||
signal,
|
||||
};
|
||||
if (args.limit !== undefined) opts.limit = args.limit;
|
||||
if (args.include_content !== undefined) opts.includeContent = args.include_content;
|
||||
const results = await this.provider.search(args.query, opts);
|
||||
const results = await this.provider.search(args.query, { toolCallId, signal });
|
||||
const builder = new ToolResultBuilder({ maxLineLength: null });
|
||||
|
||||
if (results.length === 0) {
|
||||
|
|
@ -131,7 +100,6 @@ export class WebSearchTool implements BuiltinTool<WebSearchInput> {
|
|||
if (result.date) builder.write(`Date: ${result.date}\n`);
|
||||
builder.write(`URL: ${result.url}\n`);
|
||||
builder.write(`Snippet: ${result.snippet}\n\n`);
|
||||
if (result.content) builder.write(`${result.content}\n\n`);
|
||||
}
|
||||
|
||||
// Keep the citation reminder next to the data (not just in the static tool
|
||||
|
|
|
|||
|
|
@ -70,15 +70,20 @@ export class FetchURLTool implements BuiltinTool<FetchURLInput> {
|
|||
}
|
||||
|
||||
const builder = new ToolResultBuilder({ maxLineLength: null });
|
||||
builder.write(content);
|
||||
// Tell the LLM whether it received the whole body or only the
|
||||
// extracted article text, so it can judge how complete the
|
||||
// content is.
|
||||
const message =
|
||||
// Tell the LLM whether it received the whole body or only the extracted
|
||||
// article text, so it can judge how complete the content is, and remind it
|
||||
// to cite this page when it uses the content. Both notes must ride in
|
||||
// `output`: the result's `message` field is dropped from the transcript, so
|
||||
// `output` is the only place the model can read them. Put them at the front
|
||||
// so they survive any downstream truncation of the body.
|
||||
const note =
|
||||
kind === 'passthrough'
|
||||
? 'The returned content is the full response body, returned verbatim.'
|
||||
: 'The returned content is the main text extracted from the page.';
|
||||
return builder.ok(message);
|
||||
const citeReminder =
|
||||
'If you use it in your answer, cite this page as a markdown link, e.g. [title](url).';
|
||||
builder.write(`${note} ${citeReminder}\n\n${content}`);
|
||||
return builder.ok();
|
||||
} catch (error) {
|
||||
// An in-flight abort rejects the signal-aware fetch promptly. Re-throw
|
||||
// so the executor can classify it (including user cancellation) and
|
||||
|
|
|
|||
|
|
@ -78,6 +78,35 @@ describe('FetchURLTool abort signal', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('FetchURLTool output note', () => {
|
||||
async function runKind(kind: UrlFetchResult['kind']): Promise<string> {
|
||||
const fetch = vi
|
||||
.fn<UrlFetcher['fetch']>()
|
||||
.mockResolvedValue({ content: 'BODY', kind } satisfies UrlFetchResult);
|
||||
const tool = new FetchURLTool({ fetch });
|
||||
const result = await execute(tool, 'https://example.com', new AbortController().signal);
|
||||
expect(result.isError).toBe(false);
|
||||
if (typeof result.output !== 'string') throw new Error('expected string output');
|
||||
return result.output;
|
||||
}
|
||||
|
||||
it('puts the passthrough note and citation reminder at the front of output', async () => {
|
||||
const output = await runKind('passthrough');
|
||||
expect(output).toBe(
|
||||
'The returned content is the full response body, returned verbatim. ' +
|
||||
'If you use it in your answer, cite this page as a markdown link, e.g. [title](url).\n\nBODY',
|
||||
);
|
||||
});
|
||||
|
||||
it('puts the extracted note and citation reminder at the front of output', async () => {
|
||||
const output = await runKind('extracted');
|
||||
expect(output).toBe(
|
||||
'The returned content is the main text extracted from the page. ' +
|
||||
'If you use it in your answer, cite this page as a markdown link, e.g. [title](url).\n\nBODY',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LocalFetchURLProvider abort signal', () => {
|
||||
it('passes the signal through to fetchImpl', async () => {
|
||||
const controller = new AbortController();
|
||||
|
|
|
|||
|
|
@ -678,7 +678,7 @@ describe('WebSearchProviderService', () => {
|
|||
|
||||
const provider = createService().getWebSearchProvider();
|
||||
expect(provider).not.toBeUndefined();
|
||||
const results = await provider!.search('hello', { limit: 2 });
|
||||
const results = await provider!.search('hello');
|
||||
|
||||
expect(results).toEqual([
|
||||
{ title: 'Title', url: 'https://example.com', snippet: 'Snippet' },
|
||||
|
|
@ -689,6 +689,7 @@ describe('WebSearchProviderService', () => {
|
|||
const headers = init.headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBe('Bearer access-token');
|
||||
expect(headers['X-Custom']).toBe('yes');
|
||||
expect(JSON.parse(init.body as string)).toEqual({ text_query: 'hello' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue