mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
refactor: extract provider response parsers
Change-Id: I6884999f34594faec349378deb59e68972e07355
This commit is contained in:
parent
6dd7ef15a4
commit
70f9a39cd7
6 changed files with 168 additions and 98 deletions
44
main.js
44
main.js
File diff suppressed because one or more lines are too long
14
main.ts
14
main.ts
|
|
@ -31,6 +31,14 @@ import { translate } from './src/i18n';
|
|||
import { cardsToMarkdown } from './src/markdown';
|
||||
import { activeSectionLine, nextCardIndex } from './src/navigation';
|
||||
import { buildPrompts } from './src/prompt';
|
||||
import {
|
||||
cardsFromAnthropicToolUse,
|
||||
textFromAnthropicMessagesResponse,
|
||||
textFromGoogleGenerativeAiResponse,
|
||||
textFromOpenAiChatResponse,
|
||||
textFromOpenAiResponsesResponse,
|
||||
textFromProviderContent,
|
||||
} from './src/provider-parsers';
|
||||
import {
|
||||
buildAnthropicMessagesBody,
|
||||
buildGeminiBody,
|
||||
|
|
@ -808,6 +816,12 @@ export const __test = {
|
|||
touchCacheEntry,
|
||||
translate,
|
||||
tokenLimitFieldForOpenAiChat,
|
||||
cardsFromAnthropicToolUse,
|
||||
textFromAnthropicMessagesResponse,
|
||||
textFromGoogleGenerativeAiResponse,
|
||||
textFromOpenAiChatResponse,
|
||||
textFromOpenAiResponsesResponse,
|
||||
textFromProviderContent,
|
||||
updateCardAt,
|
||||
validateBatchFolderInput,
|
||||
visibleTopProbeY,
|
||||
|
|
|
|||
87
src/provider-parsers.ts
Normal file
87
src/provider-parsers.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
import { ANTHROPIC_CARD_TOOL_NAME, normalizeCardsPayload, parseCardsJson } from './schema';
|
||||
import type { PluginSettings, RawCard } from './types';
|
||||
|
||||
export function textFromProviderContent(content: unknown): string {
|
||||
if (typeof content === 'string') return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === 'string') return part;
|
||||
if (part && typeof part === 'object') {
|
||||
const p = part as Record<string, unknown>;
|
||||
return typeof p.text === 'string' ? p.text : typeof p.output_text === 'string' ? p.output_text : '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
if (content && typeof content === 'object') {
|
||||
const c = content as Record<string, unknown>;
|
||||
return typeof c.text === 'string' ? c.text : typeof c.output_text === 'string' ? c.output_text : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function textFromOpenAiResponsesResponse(json: Record<string, unknown>): string {
|
||||
if (typeof json.output_text === 'string') return json.output_text;
|
||||
const parts: string[] = [];
|
||||
const walk = (value: unknown) => {
|
||||
if (!value) return;
|
||||
if (typeof value === 'string') return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(walk);
|
||||
return;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const v = value as Record<string, unknown>;
|
||||
if (typeof v.text === 'string') parts.push(v.text);
|
||||
if (typeof v.output_text === 'string') parts.push(v.output_text);
|
||||
if (v.type === 'output_text' && typeof v.content === 'string') parts.push(v.content);
|
||||
if (v.content) walk(v.content);
|
||||
if (v.output) walk(v.output);
|
||||
}
|
||||
};
|
||||
walk(json.output);
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
export function cardsFromAnthropicToolUse(
|
||||
json: Record<string, unknown>,
|
||||
settings?: PluginSettings | null,
|
||||
): RawCard[] | null {
|
||||
const content = Array.isArray(json?.content) ? (json.content as Array<Record<string, unknown>>) : [];
|
||||
const block = content.find((c) => c && c.type === 'tool_use' && c.name === ANTHROPIC_CARD_TOOL_NAME);
|
||||
if (!block) return null;
|
||||
if (typeof block.input === 'string') return parseCardsJson(block.input, settings);
|
||||
if (block.input && typeof block.input === 'object')
|
||||
return normalizeCardsPayload(block.input as Record<string, unknown>);
|
||||
return [];
|
||||
}
|
||||
|
||||
export function textFromAnthropicMessagesResponse(json: Record<string, unknown>): string {
|
||||
const contentBlocks = Array.isArray(json.content) ? (json.content as Array<unknown>) : [];
|
||||
return contentBlocks
|
||||
.map((c) => textFromProviderContent(c))
|
||||
.join('')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function textFromOpenAiChatResponse(json: Record<string, unknown>): string {
|
||||
const choices = Array.isArray(json.choices) ? (json.choices as Array<Record<string, unknown>>) : [];
|
||||
const choice = choices[0] || {};
|
||||
const message = choice.message as Record<string, unknown> | undefined;
|
||||
return textFromProviderContent(message?.content ?? choice.text ?? '').trim();
|
||||
}
|
||||
|
||||
export function textFromGoogleGenerativeAiResponse(json: Record<string, unknown>): string {
|
||||
const candidates = Array.isArray(json.candidates) ? (json.candidates as Array<Record<string, unknown>>) : [];
|
||||
const candidate = candidates[0] || {};
|
||||
const contentObj = candidate.content as Record<string, unknown> | undefined;
|
||||
const parts = Array.isArray(contentObj?.parts) ? (contentObj.parts as Array<unknown>) : [];
|
||||
return parts
|
||||
.map((p) => textFromProviderContent(p))
|
||||
.join('')
|
||||
.trim();
|
||||
}
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
import { translate } from './i18n';
|
||||
import {
|
||||
cardsFromAnthropicToolUse,
|
||||
textFromAnthropicMessagesResponse,
|
||||
textFromGoogleGenerativeAiResponse,
|
||||
textFromOpenAiChatResponse,
|
||||
textFromOpenAiResponsesResponse,
|
||||
} from './provider-parsers';
|
||||
import {
|
||||
ANTHROPIC_CARD_TOOL_NAME,
|
||||
anthropicCardTool,
|
||||
cardOutputSchema,
|
||||
normalizeCardsPayload,
|
||||
openAiJsonSchemaResponseFormat,
|
||||
openAiResponsesTextFormat,
|
||||
parseCardsJson,
|
||||
|
|
@ -231,50 +237,6 @@ async function requestJsonBodyWithStructuredFallback(
|
|||
}
|
||||
}
|
||||
|
||||
function textFromContent(content: unknown): string {
|
||||
if (typeof content === 'string') return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === 'string') return part;
|
||||
if (part && typeof part === 'object') {
|
||||
const p = part as Record<string, unknown>;
|
||||
return typeof p.text === 'string' ? p.text : typeof p.output_text === 'string' ? p.output_text : '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
if (content && typeof content === 'object') {
|
||||
const c = content as Record<string, unknown>;
|
||||
return typeof c.text === 'string' ? c.text : typeof c.output_text === 'string' ? c.output_text : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function textFromOpenAiResponses(json: Record<string, unknown>): string {
|
||||
if (typeof json.output_text === 'string') return json.output_text;
|
||||
const parts: string[] = [];
|
||||
const walk = (value: unknown) => {
|
||||
if (!value) return;
|
||||
if (typeof value === 'string') return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(walk);
|
||||
return;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const v = value as Record<string, unknown>;
|
||||
if (typeof v.text === 'string') parts.push(v.text);
|
||||
if (typeof v.output_text === 'string') parts.push(v.output_text);
|
||||
if (v.type === 'output_text' && typeof v.content === 'string') parts.push(v.content);
|
||||
if (v.content) walk(v.content);
|
||||
if (v.output) walk(v.output);
|
||||
}
|
||||
};
|
||||
walk(json.output);
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
export function tokenLimitFieldForOpenAiChat(settings: PluginSettings): string {
|
||||
const preset = getApiPreset(settings);
|
||||
const format = API_FORMATS[getApiFormat(settings)];
|
||||
|
|
@ -363,16 +325,6 @@ export function buildGeminiBody(
|
|||
};
|
||||
}
|
||||
|
||||
function cardsFromAnthropicToolUse(json: Record<string, unknown>, settings?: PluginSettings | null) {
|
||||
const content = Array.isArray(json?.content) ? (json.content as Array<Record<string, unknown>>) : [];
|
||||
const block = content.find((c) => c && c.type === 'tool_use' && c.name === ANTHROPIC_CARD_TOOL_NAME);
|
||||
if (!block) return null;
|
||||
if (typeof block.input === 'string') return parseCardsJson(block.input, settings);
|
||||
if (block.input && typeof block.input === 'object')
|
||||
return normalizeCardsPayload(block.input as Record<string, unknown>);
|
||||
return [];
|
||||
}
|
||||
|
||||
async function summarizeViaAnthropicMessages(
|
||||
requestUrlImpl: RequestUrlFunction,
|
||||
system: string,
|
||||
|
|
@ -393,12 +345,7 @@ async function summarizeViaAnthropicMessages(
|
|||
const toolCards = cardsFromAnthropicToolUse(json, settings);
|
||||
if (toolCards) return toolCards;
|
||||
|
||||
const contentBlocks = Array.isArray(json.content) ? (json.content as Array<unknown>) : [];
|
||||
const text = contentBlocks
|
||||
.map((c) => textFromContent(c))
|
||||
.join('')
|
||||
.trim();
|
||||
return parseCardsJson(text, settings);
|
||||
return parseCardsJson(textFromAnthropicMessagesResponse(json), settings);
|
||||
}
|
||||
|
||||
async function summarizeViaOpenAiChat(
|
||||
|
|
@ -417,11 +364,7 @@ async function summarizeViaOpenAiChat(
|
|||
buildOpenAiChatBody(system, user, settings, { structured: false }),
|
||||
settings,
|
||||
);
|
||||
const choices = Array.isArray(json.choices) ? (json.choices as Array<Record<string, unknown>>) : [];
|
||||
const choice = choices[0] || {};
|
||||
const message = choice.message as Record<string, unknown> | undefined;
|
||||
const text = textFromContent(message?.content ?? choice.text ?? '').trim();
|
||||
return parseCardsJson(text, settings);
|
||||
return parseCardsJson(textFromOpenAiChatResponse(json), settings);
|
||||
}
|
||||
|
||||
async function summarizeViaOpenAiResponses(
|
||||
|
|
@ -440,7 +383,7 @@ async function summarizeViaOpenAiResponses(
|
|||
buildOpenAiResponsesBody(system, user, settings, { structured: false }),
|
||||
settings,
|
||||
);
|
||||
return parseCardsJson(textFromOpenAiResponses(json).trim(), settings);
|
||||
return parseCardsJson(textFromOpenAiResponsesResponse(json).trim(), settings);
|
||||
}
|
||||
|
||||
async function summarizeViaGoogleGenerativeAi(
|
||||
|
|
@ -464,15 +407,7 @@ async function summarizeViaGoogleGenerativeAi(
|
|||
buildGeminiBody(system, user, settings, { structured: false }),
|
||||
settings,
|
||||
);
|
||||
const candidates = Array.isArray(json.candidates) ? (json.candidates as Array<Record<string, unknown>>) : [];
|
||||
const candidate = candidates[0] || {};
|
||||
const contentObj = candidate.content as Record<string, unknown> | undefined;
|
||||
const parts = Array.isArray(contentObj?.parts) ? (contentObj.parts as Array<unknown>) : [];
|
||||
const text = parts
|
||||
.map((p) => textFromContent(p))
|
||||
.join('')
|
||||
.trim();
|
||||
return parseCardsJson(text, settings);
|
||||
return parseCardsJson(textFromGoogleGenerativeAiResponse(json), settings);
|
||||
}
|
||||
|
||||
// Obsidian's requestUrl is not directly compatible with fetch — we accept it as a typed callback
|
||||
|
|
|
|||
|
|
@ -70,6 +70,11 @@ assert.strictEqual(typeof t.resolveCliPath, 'function');
|
|||
assert.strictEqual(typeof t.runCli, 'function');
|
||||
assert.strictEqual(typeof t.buildPrompts, 'function');
|
||||
assert.strictEqual(typeof t.buildOpenAiChatBody, 'function');
|
||||
assert.strictEqual(typeof t.textFromOpenAiChatResponse, 'function');
|
||||
assert.strictEqual(typeof t.textFromAnthropicMessagesResponse, 'function');
|
||||
assert.strictEqual(typeof t.textFromOpenAiResponsesResponse, 'function');
|
||||
assert.strictEqual(typeof t.textFromGoogleGenerativeAiResponse, 'function');
|
||||
assert.strictEqual(typeof t.cardsFromAnthropicToolUse, 'function');
|
||||
assert.strictEqual(typeof t.extractJson, 'function');
|
||||
assert.strictEqual(typeof t.findLineForAnchor, 'function');
|
||||
assert.strictEqual(typeof t.folderPathsForTarget, 'function');
|
||||
|
|
|
|||
|
|
@ -523,6 +523,35 @@ assert.strictEqual(gemBody.systemInstruction.parts[0].text, 'sys');
|
|||
assert.strictEqual(gemBody.contents[0].parts[0].text, 'usr');
|
||||
assert.strictEqual(gemBody.generationConfig.responseMimeType, 'application/json');
|
||||
|
||||
const providerCardsJson = JSON.stringify({ cards: [{ title: 'T', anchor: 'A', gist: 'G', bullets: ['B'] }] });
|
||||
assert.strictEqual(
|
||||
t.textFromOpenAiChatResponse({ choices: [{ message: { content: [{ text: providerCardsJson }] } }] }),
|
||||
providerCardsJson,
|
||||
'OpenAI Chat parser extracts message content text',
|
||||
);
|
||||
assert.strictEqual(
|
||||
t.textFromAnthropicMessagesResponse({ content: [{ type: 'text', text: providerCardsJson }] }),
|
||||
providerCardsJson,
|
||||
'Anthropic parser extracts text blocks',
|
||||
);
|
||||
assert.strictEqual(
|
||||
t.textFromOpenAiResponsesResponse({ output: [{ content: [{ type: 'output_text', text: providerCardsJson }] }] }),
|
||||
providerCardsJson,
|
||||
'OpenAI Responses parser extracts nested output text',
|
||||
);
|
||||
assert.strictEqual(
|
||||
t.textFromGoogleGenerativeAiResponse({ candidates: [{ content: { parts: [{ text: providerCardsJson }] } }] }),
|
||||
providerCardsJson,
|
||||
'Gemini parser extracts candidate part text',
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
t.cardsFromAnthropicToolUse({
|
||||
content: [{ type: 'tool_use', name: 'record_parallel_reader_cards', input: JSON.parse(providerCardsJson) }],
|
||||
}),
|
||||
[{ title: 'T', anchor: 'A', gist: 'G', bullets: ['B'] }],
|
||||
'Anthropic tool-use parser extracts structured card payloads',
|
||||
);
|
||||
|
||||
// tokenLimitFieldForOpenAiChat
|
||||
assert.strictEqual(
|
||||
t.tokenLimitFieldForOpenAiChat({ ...baseSettings, apiProvider: 'openai' }),
|
||||
|
|
|
|||
Loading…
Reference in a new issue