Path: blob/main/extensions/copilot/test/pipeline/output.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as fs from 'fs/promises';6import * as path from 'path';7import { IGeneratedPrompt } from './promptStep';8import { IProcessedRow } from './replayRecording';9import { IGeneratedResponse } from './responseStep';1011export interface IMessage {12readonly role: 'system' | 'user' | 'assistant';13readonly content: string;14}1516export interface ISampleMetadata {17readonly rowIndex: number;18readonly language: string;19readonly strategy: string;20readonly oracleEditCount: number;21readonly suggestionStatus: string;22readonly filePath: string;23readonly docContent: string;24readonly oracleEdits: readonly (readonly [start: number, endEx: number, text: string])[];25readonly originalPrompt: unknown[];26readonly modelResponse: string;27}2829export interface ISample {30readonly messages: readonly IMessage[];31readonly metadata: ISampleMetadata;32}3334interface ISkipReason {35readonly rowIndex: number;36readonly reason: string;37}3839export interface IWriteResult {40readonly written: number;41readonly skipped: number;42readonly skipReasons: readonly ISkipReason[];43readonly fileSize: number;44readonly outputPath: string;45readonly languageCounts: ReadonlyMap<string, number>;46}4748export function assembleSample(49index: number,50prompt: IGeneratedPrompt,51response: IGeneratedResponse,52processedRow: IProcessedRow,53strategy: string,54modelResponse: string,55): ISample {56const messages: IMessage[] = [57{ role: 'system', content: prompt.system },58{ role: 'user', content: prompt.user },59{ role: 'assistant', content: response.assistant },60];6162const metadata: ISampleMetadata = {63rowIndex: index,64language: processedRow.row.activeDocumentLanguageId,65strategy,66oracleEditCount: processedRow.nextUserEdit?.edit?.length ?? 0,67suggestionStatus: processedRow.row.suggestionStatus,68filePath: processedRow.activeFilePath.replace(/\\/g, '/'),69docContent: processedRow.activeDocument.value.get().value,70oracleEdits: processedRow.nextUserEdit?.edit ?? [],71originalPrompt: processedRow.row.prompt,72modelResponse,73};7475return { messages, metadata };76}7778interface IStructuralValidationResult {79readonly valid: boolean;80readonly reason?: string;81}8283/**84* Structural check: ensures messages are non-empty before writing.85*/86export function validateSample(sample: ISample): IStructuralValidationResult {87for (const msg of sample.messages) {88if (msg.content === undefined || msg.content === null) {89return { valid: false, reason: `${msg.role} message content is null/undefined` };90}91}9293const system = sample.messages.find(m => m.role === 'system');94const user = sample.messages.find(m => m.role === 'user');95const assistant = sample.messages.find(m => m.role === 'assistant');9697if (!system || !system.content.trim()) {98return { valid: false, reason: 'Empty system message' };99}100if (!user || !user.content.trim()) {101return { valid: false, reason: 'Empty user message' };102}103if (!assistant || !assistant.content.trim()) {104return { valid: false, reason: 'Empty assistant message' };105}106107return { valid: true };108}109110export function resolveOutputPath(inputPath: string, explicitPath: string | undefined): string {111if (explicitPath) {112return path.resolve(explicitPath);113}114const parsed = path.parse(inputPath);115return path.join(parsed.dir, `${parsed.name}_output.json`);116}117118/**119* Write validated samples to a JSON file.120* Samples are sorted by rowIndex for deterministic output.121*/122export async function writeSamples(123outputPath: string,124samples: readonly ISample[],125): Promise<IWriteResult> {126const skipReasons: ISkipReason[] = [];127const validSamples: ISample[] = [];128129for (const sample of samples) {130const result = validateSample(sample);131if (result.valid) {132validSamples.push(sample);133} else {134skipReasons.push({135rowIndex: sample.metadata.rowIndex,136reason: result.reason!,137});138}139}140141validSamples.sort((a, b) => a.metadata.rowIndex - b.metadata.rowIndex);142143const output = validSamples.map(sample => ({144messages: sample.messages.map(m => ({ role: m.role, content: m.content })),145metadata: sample.metadata,146}));147const content = JSON.stringify(output, null, 2);148149const resolvedPath = path.resolve(outputPath);150await fs.mkdir(path.dirname(resolvedPath), { recursive: true });151await fs.writeFile(resolvedPath, content, 'utf-8');152153const fileSize = Buffer.byteLength(content, 'utf-8');154const languageCounts = new Map<string, number>();155for (const sample of validSamples) {156const lang = sample.metadata.language || 'unknown';157languageCounts.set(lang, (languageCounts.get(lang) ?? 0) + 1);158}159160return {161written: validSamples.length,162skipped: skipReasons.length,163skipReasons,164fileSize,165outputPath: resolvedPath,166languageCounts,167};168}169170171