From 53968f6f8c00861bf4d9e7a778e7f04a5a518fd2 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Fri, 4 Jul 2025 07:32:30 +0800 Subject: [PATCH] feat(server): edit tool intent collect (#12998) ## Summary by CodeRabbit * **New Features** * Footnotes are now included in streamed AI responses, formatted as markdown and appended at the end of the output when available. * **Improvements** * Enhanced handling of footnotes across multiple AI providers, ensuring consistent display of additional information after the main response. * **Refactor** * Removed citation parsing from one provider to streamline output. --- .../copilot/providers/anthropic/anthropic.ts | 6 +++++ .../copilot/providers/gemini/gemini.ts | 6 +++++ .../src/plugins/copilot/providers/morph.ts | 9 +------ .../src/plugins/copilot/providers/openai.ts | 4 +++- .../src/plugins/copilot/providers/utils.ts | 24 +++++++++++++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/backend/server/src/plugins/copilot/providers/anthropic/anthropic.ts b/packages/backend/server/src/plugins/copilot/providers/anthropic/anthropic.ts index 0832b4020..7e2cb554f 100644 --- a/packages/backend/server/src/plugins/copilot/providers/anthropic/anthropic.ts +++ b/packages/backend/server/src/plugins/copilot/providers/anthropic/anthropic.ts @@ -108,6 +108,12 @@ export abstract class AnthropicProvider extends CopilotProvider { break; } } + if (!options.signal?.aborted) { + const footnotes = parser.end(); + if (footnotes.length) { + yield `\n\n${footnotes}`; + } + } } catch (e: any) { metrics.ai.counter('chat_text_stream_errors').add(1, { model: model.id }); throw this.handleError(e); diff --git a/packages/backend/server/src/plugins/copilot/providers/gemini/gemini.ts b/packages/backend/server/src/plugins/copilot/providers/gemini/gemini.ts index 33bdd817e..7cb11fece 100644 --- a/packages/backend/server/src/plugins/copilot/providers/gemini/gemini.ts +++ b/packages/backend/server/src/plugins/copilot/providers/gemini/gemini.ts @@ -166,6 +166,12 @@ export abstract class GeminiProvider extends CopilotProvider { break; } } + if (!options.signal?.aborted) { + const footnotes = parser.end(); + if (footnotes.length) { + yield `\n\n${footnotes}`; + } + } } catch (e: any) { metrics.ai.counter('chat_text_stream_errors').add(1, { model: model.id }); throw this.handleError(e); diff --git a/packages/backend/server/src/plugins/copilot/providers/morph.ts b/packages/backend/server/src/plugins/copilot/providers/morph.ts index c832dda4b..6d41f3f56 100644 --- a/packages/backend/server/src/plugins/copilot/providers/morph.ts +++ b/packages/backend/server/src/plugins/copilot/providers/morph.ts @@ -16,7 +16,7 @@ import type { PromptMessage, } from './types'; import { CopilotProviderType, ModelInputType, ModelOutputType } from './types'; -import { chatToGPTMessage, CitationParser, TextStreamParser } from './utils'; +import { chatToGPTMessage, TextStreamParser } from './utils'; export const DEFAULT_DIMENSIONS = 256; @@ -130,18 +130,11 @@ export class MorphProvider extends CopilotProvider { abortSignal: options.signal, }); - const citationParser = new CitationParser(); const textParser = new TextStreamParser(); for await (const chunk of fullStream) { switch (chunk.type) { case 'text-delta': { let result = textParser.parse(chunk); - result = citationParser.parse(result); - yield result; - break; - } - case 'finish': { - const result = citationParser.end(); yield result; break; } diff --git a/packages/backend/server/src/plugins/copilot/providers/openai.ts b/packages/backend/server/src/plugins/copilot/providers/openai.ts index 0f57d4378..8f6e3d232 100644 --- a/packages/backend/server/src/plugins/copilot/providers/openai.ts +++ b/packages/backend/server/src/plugins/copilot/providers/openai.ts @@ -347,7 +347,9 @@ export class OpenAIProvider extends CopilotProvider { break; } case 'finish': { - const result = citationParser.end(); + const footnotes = textParser.end(); + const result = + citationParser.end() + (footnotes.length ? '\n' + footnotes : ''); yield result; break; } diff --git a/packages/backend/server/src/plugins/copilot/providers/utils.ts b/packages/backend/server/src/plugins/copilot/providers/utils.ts index 949e26fbf..e4f083503 100644 --- a/packages/backend/server/src/plugins/copilot/providers/utils.ts +++ b/packages/backend/server/src/plugins/copilot/providers/utils.ts @@ -412,6 +412,10 @@ export function toError(error: unknown): Error { } } +type DocEditFootnote = { + intent: string; + result: string; +}; export class TextStreamParser { private readonly logger = new Logger(TextStreamParser.name); private readonly CALLOUT_PREFIX = '\n[!]\n'; @@ -420,6 +424,8 @@ export class TextStreamParser { private prefix: string | null = this.CALLOUT_PREFIX; + private readonly docEditFootnotes: DocEditFootnote[] = []; + public parse(chunk: TextStreamPart) { let result = ''; switch (chunk.type) { @@ -463,6 +469,13 @@ export class TextStreamParser { result += `\nWriting document "${chunk.args.title}"\n`; break; } + case 'doc_edit': { + this.docEditFootnotes.push({ + intent: chunk.args.instructions, + result: '', + }); + break; + } } result = this.markAsCallout(result); break; @@ -476,6 +489,10 @@ export class TextStreamParser { case 'doc_edit': { if (chunk.result && typeof chunk.result === 'object') { result += `\n${chunk.result.result}\n`; + this.docEditFootnotes[this.docEditFootnotes.length - 1].result = + chunk.result.result; + } else { + this.docEditFootnotes.pop(); } break; } @@ -520,6 +537,13 @@ export class TextStreamParser { return result; } + public end() { + const footnotes = this.docEditFootnotes.map((footnote, index) => { + return `[^edit${index + 1}]: ${JSON.stringify({ type: 'doc-edit', ...footnote })}`; + }); + return footnotes.join('\n'); + } + private addPrefix(text: string) { if (this.prefix) { const result = this.prefix + text;