mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
feat(core): mark reasoning summary as markdown callout (#12176)
Close [AI-75](https://linear.app/affine-design/issue/AI-75)  <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved formatting for AI-generated reasoning and web search results, displaying them as callout blocks for enhanced readability. - Expanded support for rendering callout blocks in the text renderer. - **Style** - Adjusted layout to ensure callout blocks retain appropriate spacing and appearance. - **Refactor** - Simplified and unified the integration of web search tools for AI providers. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
metrics,
|
metrics,
|
||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
import { createExaTool } from '../tools';
|
import { createExaSearchTool } from '../tools';
|
||||||
import { CopilotProvider } from './provider';
|
import { CopilotProvider } from './provider';
|
||||||
import {
|
import {
|
||||||
ChatMessageRole,
|
ChatMessageRole,
|
||||||
@@ -38,7 +38,7 @@ export class AnthropicProvider
|
|||||||
|
|
||||||
private readonly MAX_STEPS = 20;
|
private readonly MAX_STEPS = 20;
|
||||||
|
|
||||||
private toolResults: string[] = [];
|
private readonly CALLOUT_PREFIX = '\n> [!]\n> ';
|
||||||
|
|
||||||
#instance!: AnthropicSDKProvider;
|
#instance!: AnthropicSDKProvider;
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export class AnthropicProvider
|
|||||||
providerOptions: {
|
providerOptions: {
|
||||||
anthropic: this.getAnthropicOptions(options),
|
anthropic: this.getAnthropicOptions(options),
|
||||||
},
|
},
|
||||||
tools: this.getTools(options),
|
tools: this.getTools(),
|
||||||
maxSteps: this.MAX_STEPS,
|
maxSteps: this.MAX_STEPS,
|
||||||
experimental_continueSteps: true,
|
experimental_continueSteps: true,
|
||||||
});
|
});
|
||||||
@@ -166,42 +166,63 @@ export class AnthropicProvider
|
|||||||
providerOptions: {
|
providerOptions: {
|
||||||
anthropic: this.getAnthropicOptions(options),
|
anthropic: this.getAnthropicOptions(options),
|
||||||
},
|
},
|
||||||
tools: this.getTools(options),
|
tools: this.getTools(),
|
||||||
maxSteps: this.MAX_STEPS,
|
maxSteps: this.MAX_STEPS,
|
||||||
experimental_continueSteps: true,
|
experimental_continueSteps: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const message of fullStream) {
|
let lastType;
|
||||||
switch (message.type) {
|
// reasoning, tool-call, tool-result need to mark as callout
|
||||||
|
let prefix: string | null = this.CALLOUT_PREFIX;
|
||||||
|
for await (const chunk of fullStream) {
|
||||||
|
switch (chunk.type) {
|
||||||
|
case 'text-delta': {
|
||||||
|
if (!prefix) {
|
||||||
|
prefix = this.CALLOUT_PREFIX;
|
||||||
|
}
|
||||||
|
let result = chunk.textDelta;
|
||||||
|
if (lastType !== chunk.type) {
|
||||||
|
result = '\n\n' + result;
|
||||||
|
}
|
||||||
|
yield result;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'reasoning': {
|
case 'reasoning': {
|
||||||
yield message.textDelta;
|
if (prefix) {
|
||||||
|
yield prefix;
|
||||||
|
prefix = null;
|
||||||
|
}
|
||||||
|
let result = chunk.textDelta;
|
||||||
|
if (lastType !== chunk.type) {
|
||||||
|
result = '\n\n' + result;
|
||||||
|
}
|
||||||
|
yield this.markAsCallout(result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tool-call': {
|
case 'tool-call': {
|
||||||
if (message.toolName === 'web_search') {
|
if (prefix) {
|
||||||
yield '\n' + `Searching the web "${message.args.query}"` + '\n';
|
yield prefix;
|
||||||
|
prefix = null;
|
||||||
|
}
|
||||||
|
if (chunk.toolName === 'web_search') {
|
||||||
|
yield this.markAsCallout(
|
||||||
|
`\nSearching the web "${chunk.args.query}"\n`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tool-result': {
|
case 'tool-result': {
|
||||||
if (message.toolName === 'web_search') {
|
if (chunk.toolName === 'web_search') {
|
||||||
this.toolResults.push(this.getWebSearchLinks(message.result));
|
if (prefix) {
|
||||||
|
yield prefix;
|
||||||
|
prefix = null;
|
||||||
|
}
|
||||||
|
yield this.markAsCallout(this.getWebSearchLinks(chunk.result));
|
||||||
}
|
}
|
||||||
break;
|
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': {
|
case 'error': {
|
||||||
const error = message.error as { type: string; message: string };
|
const error = chunk.error as { type: string; message: string };
|
||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +230,7 @@ export class AnthropicProvider
|
|||||||
await fullStream.cancel();
|
await fullStream.cancel();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
lastType = chunk.type;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
metrics.ai.counter('chat_text_stream_errors').add(1, { model });
|
metrics.ai.counter('chat_text_stream_errors').add(1, { model });
|
||||||
@@ -216,13 +238,10 @@ export class AnthropicProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTools(options: CopilotChatOptions) {
|
private getTools() {
|
||||||
if (options?.webSearch) {
|
return {
|
||||||
return {
|
web_search: createExaSearchTool(this.AFFiNEConfig),
|
||||||
web_search: createExaTool(this.AFFiNEConfig),
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnthropicOptions(options: CopilotChatOptions) {
|
private getAnthropicOptions(options: CopilotChatOptions) {
|
||||||
@@ -243,8 +262,12 @@ export class AnthropicProvider
|
|||||||
}[]
|
}[]
|
||||||
): string {
|
): string {
|
||||||
const links = list.reduce((acc, result) => {
|
const links = list.reduce((acc, result) => {
|
||||||
return acc + `\n[${result.title ?? result.url}](${result.url})\n`;
|
return acc + `\n[${result.title ?? result.url}](${result.url})\n\n`;
|
||||||
}, '\n');
|
}, '');
|
||||||
return links + '\n';
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
private markAsCallout(text: string) {
|
||||||
|
return text.replaceAll('\n', '\n> ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
metrics,
|
metrics,
|
||||||
UserFriendlyError,
|
UserFriendlyError,
|
||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
import { createExaTool } from '../tools';
|
import { createExaSearchTool } from '../tools';
|
||||||
import { CopilotProvider } from './provider';
|
import { CopilotProvider } from './provider';
|
||||||
import {
|
import {
|
||||||
ChatMessageRole,
|
ChatMessageRole,
|
||||||
@@ -45,7 +45,7 @@ export type OpenAIConfig = {
|
|||||||
|
|
||||||
type OpenAITools = {
|
type OpenAITools = {
|
||||||
web_search_preview: ReturnType<typeof openai.tools.webSearchPreview>;
|
web_search_preview: ReturnType<typeof openai.tools.webSearchPreview>;
|
||||||
web_search: ReturnType<typeof createExaTool>;
|
web_search_exa: ReturnType<typeof createExaSearchTool>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class OpenAIProvider
|
export class OpenAIProvider
|
||||||
@@ -89,6 +89,8 @@ export class OpenAIProvider
|
|||||||
|
|
||||||
private readonly MAX_STEPS = 20;
|
private readonly MAX_STEPS = 20;
|
||||||
|
|
||||||
|
private readonly CALLOUT_PREFIX = '\n> [!]\n> ';
|
||||||
|
|
||||||
#instance!: VercelOpenAIProvider;
|
#instance!: VercelOpenAIProvider;
|
||||||
|
|
||||||
override configured(): boolean {
|
override configured(): boolean {
|
||||||
@@ -198,7 +200,7 @@ export class OpenAIProvider
|
|||||||
case 'webSearch': {
|
case 'webSearch': {
|
||||||
// o series reasoning models
|
// o series reasoning models
|
||||||
if (model.startsWith('o')) {
|
if (model.startsWith('o')) {
|
||||||
tools.web_search = createExaTool(this.AFFiNEConfig);
|
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
||||||
} else {
|
} else {
|
||||||
tools.web_search_preview = openai.tools.webSearchPreview();
|
tools.web_search_preview = openai.tools.webSearchPreview();
|
||||||
}
|
}
|
||||||
@@ -292,37 +294,52 @@ export class OpenAIProvider
|
|||||||
|
|
||||||
const parser = new CitationParser();
|
const parser = new CitationParser();
|
||||||
let lastType;
|
let lastType;
|
||||||
|
// reasoning, tool-call, tool-result need to mark as callout
|
||||||
|
let prefix: string | null = this.CALLOUT_PREFIX;
|
||||||
for await (const chunk of fullStream) {
|
for await (const chunk of fullStream) {
|
||||||
if (chunk) {
|
if (chunk) {
|
||||||
switch (chunk.type) {
|
switch (chunk.type) {
|
||||||
case 'text-delta': {
|
case 'text-delta': {
|
||||||
|
let result = parser.parse(chunk.textDelta);
|
||||||
if (lastType !== chunk.type) {
|
if (lastType !== chunk.type) {
|
||||||
yield `\n`;
|
result = '\n\n' + result;
|
||||||
}
|
}
|
||||||
const result = parser.parse(chunk.textDelta);
|
|
||||||
yield result;
|
yield result;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'reasoning': {
|
case 'reasoning': {
|
||||||
if (lastType !== chunk.type) {
|
if (prefix) {
|
||||||
yield `\n`;
|
yield prefix;
|
||||||
|
prefix = null;
|
||||||
}
|
}
|
||||||
yield chunk.textDelta;
|
let result = chunk.textDelta;
|
||||||
|
if (lastType !== chunk.type) {
|
||||||
|
result = '\n\n' + result;
|
||||||
|
}
|
||||||
|
yield this.markAsCallout(result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tool-call': {
|
case 'tool-call': {
|
||||||
if (chunk.toolName === 'web_search') {
|
if (prefix) {
|
||||||
yield '\n' + `Searching the web "${chunk.args.query}"` + '\n';
|
yield prefix;
|
||||||
|
prefix = null;
|
||||||
|
}
|
||||||
|
if (chunk.toolName === 'web_search_exa') {
|
||||||
|
yield this.markAsCallout(
|
||||||
|
`\nSearching the web "${chunk.args.query}"\n`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tool-result': {
|
case 'tool-result': {
|
||||||
if (chunk.toolName === 'web_search') {
|
if (chunk.toolName === 'web_search_exa') {
|
||||||
yield '\n' + this.getWebSearchLinks(chunk.result) + '\n';
|
yield this.markAsCallout(
|
||||||
|
`\n${this.getWebSearchLinks(chunk.result)}\n`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'step-finish': {
|
case 'finish': {
|
||||||
const result = parser.end();
|
const result = parser.end();
|
||||||
yield result;
|
yield result;
|
||||||
break;
|
break;
|
||||||
@@ -430,7 +447,7 @@ export class OpenAIProvider
|
|||||||
const result: OpenAIResponsesProviderOptions = {};
|
const result: OpenAIResponsesProviderOptions = {};
|
||||||
if (options?.reasoning) {
|
if (options?.reasoning) {
|
||||||
result.reasoningEffort = 'medium';
|
result.reasoningEffort = 'medium';
|
||||||
result.reasoningSummary = 'auto';
|
result.reasoningSummary = 'detailed';
|
||||||
}
|
}
|
||||||
if (options?.user) {
|
if (options?.user) {
|
||||||
result.user = options.user;
|
result.user = options.user;
|
||||||
@@ -445,8 +462,12 @@ export class OpenAIProvider
|
|||||||
}[]
|
}[]
|
||||||
): string {
|
): string {
|
||||||
const links = list.reduce((acc, result) => {
|
const links = list.reduce((acc, result) => {
|
||||||
return acc + `\n[${result.title ?? result.url}](${result.url})\n`;
|
return acc + `\n[${result.title ?? result.url}](${result.url})\n\n`;
|
||||||
}, '\n');
|
}, '');
|
||||||
return links + '\n';
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
private markAsCallout(text: string) {
|
||||||
|
return text.replaceAll('\n', '\n> ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { Config } from '../../../base';
|
import { Config } from '../../../base';
|
||||||
|
|
||||||
export const createExaTool = (config: Config) => {
|
export const createExaSearchTool = (config: Config) => {
|
||||||
return tool({
|
return tool({
|
||||||
description: 'Search the web for information',
|
description: 'Search the web for information',
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
.ai-answer-text-editor {
|
.ai-answer-text-editor {
|
||||||
.affine-note-block-container {
|
.affine-note-block-container {
|
||||||
> .affine-block-children-container {
|
> .affine-block-children-container {
|
||||||
> :first-child,
|
> :first-child:not(affine-callout),
|
||||||
> :first-child * {
|
> :first-child:not(affine-callout) * {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
> :last-child,
|
> :last-child,
|
||||||
@@ -225,6 +225,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
'affine:table',
|
'affine:table',
|
||||||
'affine:surface',
|
'affine:surface',
|
||||||
'affine:paragraph',
|
'affine:paragraph',
|
||||||
|
'affine:callout',
|
||||||
'affine:code',
|
'affine:code',
|
||||||
'affine:list',
|
'affine:list',
|
||||||
'affine:divider',
|
'affine:divider',
|
||||||
|
|||||||
Reference in New Issue
Block a user