feat(server): edit tool intent collect (#12998)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-07-04 07:32:30 +08:00
committed by GitHub
parent cfc108613c
commit 53968f6f8c
5 changed files with 40 additions and 9 deletions

View File

@@ -108,6 +108,12 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
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);

View File

@@ -166,6 +166,12 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
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);

View File

@@ -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<MorphConfig> {
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;
}

View File

@@ -347,7 +347,9 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
break;
}
case 'finish': {
const result = citationParser.end();
const footnotes = textParser.end();
const result =
citationParser.end() + (footnotes.length ? '\n' + footnotes : '');
yield result;
break;
}

View File

@@ -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<CustomAITools>) {
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;