fix: комментарий ИИ разбирается при любой форме структурированного ответа
Тот же промпт вернул другую структуру: массив из одного объекта, список поставщиков suppliers_analysis вместо suppliers_evaluation, плюсы/минусы строкой вместо массива — и снова показывался сырой JSON. Разбор стал терпимым: объект или массив объектов, список поставщиков — любое поле- массив объектов с supplier, плюсы/минусы — строка или массив, плюс recommendation.
This commit is contained in:
@@ -53,6 +53,38 @@ describe('aiCommentToText (ответ потока «Комментарий ИИ
|
||||
expect(aiCommentToText(text)).toBe(text);
|
||||
});
|
||||
|
||||
// Тот же промпт в другой раз вернул массив, другое имя списка и строки вместо массивов.
|
||||
it('вариант ответа: массив, suppliers_analysis, плюсы/минусы строкой', () => {
|
||||
const variant = JSON.stringify([{
|
||||
summary: 'Анализ поставщиков по критериям цены и условий поставки',
|
||||
suppliers_analysis: [
|
||||
{ supplier: 'BuildCore Supply Ltd.', strengths: 'Самые низкие цены.', weaknesses: 'Самый длительный срок.' },
|
||||
],
|
||||
conclusion: 'ТОО «СтройЛайн KZ» — оптимальный выбор.',
|
||||
}]);
|
||||
|
||||
expect(aiCommentToText(variant)).toBe([
|
||||
'Анализ поставщиков по критериям цены и условий поставки',
|
||||
'',
|
||||
'BuildCore Supply Ltd.',
|
||||
'Плюсы:',
|
||||
'- Самые низкие цены.',
|
||||
'Минусы:',
|
||||
'- Самый длительный срок.',
|
||||
'',
|
||||
'Вывод: ТОО «СтройЛайн KZ» — оптимальный выбор.',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('рекомендация — отдельной строкой', () => {
|
||||
expect(aiCommentToText('{"summary":"Итоги.","recommendation":"Берём СтройЛайн."}'))
|
||||
.toBe('Итоги.\n\nРекомендация: Берём СтройЛайн.');
|
||||
});
|
||||
|
||||
it('массив не из объектов — без изменений', () => {
|
||||
expect(aiCommentToText('["a","b"]')).toBe('["a","b"]');
|
||||
});
|
||||
|
||||
it('частично заполненная структура — только то, что есть', () => {
|
||||
expect(aiCommentToText('{"conclusion":"Берём BuildCore."}')).toBe('Вывод: Берём BuildCore.');
|
||||
});
|
||||
|
||||
@@ -1,56 +1,75 @@
|
||||
// Оценка одного поставщика в структурированном ответе потока «Комментарий ИИ».
|
||||
interface SupplierEvaluation {
|
||||
supplier?: string;
|
||||
strengths?: string[];
|
||||
weaknesses?: string[];
|
||||
}
|
||||
|
||||
// Структурированный ответ потока (промпт отвечает JSON, а не текстом).
|
||||
interface StructuredAiComment {
|
||||
summary?: string;
|
||||
suppliers_evaluation?: SupplierEvaluation[];
|
||||
conclusion?: string;
|
||||
}
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
const LABEL_STRENGTHS = 'Плюсы:';
|
||||
const LABEL_WEAKNESSES = 'Минусы:';
|
||||
const LABEL_CONCLUSION = 'Вывод:';
|
||||
const LABEL_RECOMMENDATION = 'Рекомендация:';
|
||||
|
||||
function tryParseObject(raw: string): Record<string, unknown> | undefined {
|
||||
function isObject(value: unknown): value is JsonObject {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function tryParseJson(raw: string): unknown {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed.startsWith('{')) return undefined;
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return undefined;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: undefined;
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isStructured(obj: Record<string, unknown>): boolean {
|
||||
return 'summary' in obj || 'suppliers_evaluation' in obj || 'conclusion' in obj;
|
||||
function asText(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function bulletList(label: string, items: string[] | undefined): string[] {
|
||||
if (!items || items.length === 0) return [];
|
||||
return [label, ...items.map(item => `- ${item}`)];
|
||||
// Плюсы/минусы модель отдаёт то массивом строк, то одной строкой.
|
||||
function asList(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return value.map(asText).filter((item): item is string => item !== undefined);
|
||||
const text = asText(value);
|
||||
return text ? [text] : [];
|
||||
}
|
||||
|
||||
function supplierBlock(evaluation: SupplierEvaluation): string[] {
|
||||
// Список поставщиков модель называет по-разному (suppliers_evaluation, suppliers_analysis…) —
|
||||
// ищем первое поле-массив из объектов с полем supplier.
|
||||
function findSupplierList(obj: JsonObject): JsonObject[] {
|
||||
for (const value of Object.values(obj)) {
|
||||
if (Array.isArray(value) && value.length > 0 && value.every(item => isObject(item) && 'supplier' in item)) {
|
||||
return value as JsonObject[];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function isStructured(obj: JsonObject): boolean {
|
||||
return 'summary' in obj || 'conclusion' in obj || 'recommendation' in obj || findSupplierList(obj).length > 0;
|
||||
}
|
||||
|
||||
function bulletList(label: string, items: string[]): string[] {
|
||||
return items.length > 0 ? [label, ...items.map(item => `- ${item}`)] : [];
|
||||
}
|
||||
|
||||
function supplierBlock(supplier: JsonObject): string[] {
|
||||
const name = asText(supplier.supplier);
|
||||
return [
|
||||
...(evaluation.supplier ? [evaluation.supplier] : []),
|
||||
...bulletList(LABEL_STRENGTHS, evaluation.strengths),
|
||||
...bulletList(LABEL_WEAKNESSES, evaluation.weaknesses),
|
||||
...(name ? [name] : []),
|
||||
...bulletList(LABEL_STRENGTHS, asList(supplier.strengths)),
|
||||
...bulletList(LABEL_WEAKNESSES, asList(supplier.weaknesses)),
|
||||
];
|
||||
}
|
||||
|
||||
function structuredToText(comment: StructuredAiComment): string {
|
||||
function labeled(label: string, value: unknown): string[] {
|
||||
const text = asText(value);
|
||||
return text ? [`${label} ${text}`] : [];
|
||||
}
|
||||
|
||||
function structuredToText(obj: JsonObject): string {
|
||||
const summary = asText(obj.summary);
|
||||
const blocks: string[][] = [
|
||||
comment.summary ? [comment.summary] : [],
|
||||
...(comment.suppliers_evaluation ?? []).map(supplierBlock),
|
||||
comment.conclusion ? [`${LABEL_CONCLUSION} ${comment.conclusion}`] : [],
|
||||
summary ? [summary] : [],
|
||||
...findSupplierList(obj).map(supplierBlock),
|
||||
labeled(LABEL_CONCLUSION, obj.conclusion),
|
||||
labeled(LABEL_RECOMMENDATION, obj.recommendation),
|
||||
];
|
||||
return blocks
|
||||
.filter(block => block.length > 0)
|
||||
@@ -59,13 +78,19 @@ function structuredToText(comment: StructuredAiComment): string {
|
||||
}
|
||||
|
||||
// Ответ потока «Комментарий ИИ» → текст для показа и для SP (из SP его берёт и PDF-отчёт, поэтому
|
||||
// без Markdown-звёздочек: только строки и списки «- »). Поток отвечал по-разному: просто текстом,
|
||||
// JSON {comment}, а теперь структурой {summary, suppliers_evaluation, conclusion} — её и разворачиваем.
|
||||
// без Markdown-звёздочек: только строки и списки «- »). Поток отвечал по-разному: текстом, JSON
|
||||
// {comment}, а теперь структурой, форму которой модель каждый раз выбирает сама: объект или массив
|
||||
// из одного объекта, список поставщиков под разными именами, плюсы/минусы массивом или строкой.
|
||||
// Всё, что не распознали, возвращаем как есть: лучше показать сырой ответ, чем потерять его.
|
||||
export function aiCommentToText(raw: string): string {
|
||||
const obj = tryParseObject(raw);
|
||||
if (!obj) return raw;
|
||||
const parsed = tryParseJson(raw);
|
||||
const objects: unknown[] = Array.isArray(parsed) ? parsed : [parsed];
|
||||
if (objects.length === 0 || !objects.every(isObject)) return raw;
|
||||
|
||||
const texts = (objects as JsonObject[]).map(obj => {
|
||||
if (typeof obj.comment === 'string') return obj.comment;
|
||||
if (isStructured(obj)) return structuredToText(obj as StructuredAiComment);
|
||||
return raw;
|
||||
return isStructured(obj) ? structuredToText(obj) : undefined;
|
||||
});
|
||||
if (texts.some(text => text === undefined)) return raw;
|
||||
return texts.join('\n\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user