mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 10:06:17 +08:00
feat(core): add web search tool and reasoning params (#11912)
Close [AI-60](https://linear.app/affine-design/issue/AI-60). ### What changed? - Add Exa web search tool - Add reasoning params
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AnthropicProvider as AnthropicSDKProvider,
|
||||
AnthropicProviderOptions,
|
||||
createAnthropic,
|
||||
} from '@ai-sdk/anthropic';
|
||||
import { AISDKError, generateText, streamText } from 'ai';
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
metrics,
|
||||
UserFriendlyError,
|
||||
} from '../../../base';
|
||||
import { createExaTool } from '../tools';
|
||||
import { CopilotProvider } from './provider';
|
||||
import {
|
||||
ChatMessageRole,
|
||||
@@ -34,6 +36,10 @@ export class AnthropicProvider
|
||||
override readonly capabilities = [CopilotCapability.TextToText];
|
||||
override readonly models = ['claude-3-7-sonnet-20250219'];
|
||||
|
||||
private readonly MAX_STEPS = 20;
|
||||
|
||||
private toolResults: string[] = [];
|
||||
|
||||
#instance!: AnthropicSDKProvider;
|
||||
|
||||
override configured(): boolean {
|
||||
@@ -120,15 +126,24 @@ export class AnthropicProvider
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
|
||||
const modelInstance = this.#instance(model);
|
||||
const { text } = await generateText({
|
||||
const { text, reasoning } = await generateText({
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
abortSignal: options.signal,
|
||||
providerOptions: {
|
||||
anthropic: this.getAnthropicOptions(options),
|
||||
},
|
||||
tools: {
|
||||
webSearch: createExaTool(this.AFFiNEConfig),
|
||||
},
|
||||
maxSteps: this.MAX_STEPS,
|
||||
experimental_continueSteps: true,
|
||||
});
|
||||
|
||||
if (!text) throw new Error('Failed to generate text');
|
||||
return text.trim();
|
||||
|
||||
return reasoning ? `${reasoning}\n${text}` : text;
|
||||
} catch (e: any) {
|
||||
metrics.ai.counter('chat_text_errors').add(1, { model });
|
||||
throw this.handleError(e);
|
||||
@@ -145,21 +160,52 @@ export class AnthropicProvider
|
||||
try {
|
||||
metrics.ai.counter('chat_text_stream_calls').add(1, { model });
|
||||
const [system, msgs] = await chatToGPTMessage(messages);
|
||||
|
||||
const { textStream } = streamText({
|
||||
const { fullStream } = streamText({
|
||||
model: this.#instance(model),
|
||||
system,
|
||||
messages: msgs,
|
||||
abortSignal: options.signal,
|
||||
providerOptions: {
|
||||
anthropic: this.getAnthropicOptions(options),
|
||||
},
|
||||
tools: {
|
||||
webSearch: createExaTool(this.AFFiNEConfig),
|
||||
},
|
||||
maxSteps: this.MAX_STEPS,
|
||||
experimental_continueSteps: true,
|
||||
});
|
||||
|
||||
for await (const message of textStream) {
|
||||
if (message) {
|
||||
yield message;
|
||||
if (options.signal?.aborted) {
|
||||
await textStream.cancel();
|
||||
for await (const message of fullStream) {
|
||||
switch (message.type) {
|
||||
case 'reasoning': {
|
||||
yield message.textDelta;
|
||||
break;
|
||||
}
|
||||
case 'tool-result': {
|
||||
if (message.toolName === 'webSearch') {
|
||||
this.toolResults.push(this.getWebSearchLinks(message.result));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'step-finish': {
|
||||
if (message.finishReason === 'tool-calls') {
|
||||
yield this.toolResults.join('\n');
|
||||
this.toolResults = [];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'text-delta': {
|
||||
yield message.textDelta;
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
const error = message.error as { type: string; message: string };
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
await fullStream.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -167,4 +213,30 @@ export class AnthropicProvider
|
||||
throw this.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private getAnthropicOptions(
|
||||
options: CopilotChatOptions
|
||||
): AnthropicProviderOptions {
|
||||
if (options?.reasoning) {
|
||||
return {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budgetTokens: 12000,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private getWebSearchLinks(
|
||||
list: {
|
||||
title: string | null;
|
||||
url: string;
|
||||
}[]
|
||||
): string {
|
||||
const links = list.reduce((acc, result) => {
|
||||
return acc + `\n[${result.title ?? result.url}](${result.url})\n`;
|
||||
}, '\n');
|
||||
return links + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,11 @@ const CopilotProviderOptionsSchema = z.object({
|
||||
|
||||
const CopilotChatOptionsSchema = CopilotProviderOptionsSchema.merge(
|
||||
PromptConfigStrictSchema
|
||||
).optional();
|
||||
)
|
||||
.extend({
|
||||
reasoning: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type CopilotChatOptions = z.infer<typeof CopilotChatOptionsSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user