feat: use visual captions in assistant sources
This commit is contained in:
parent
dd29ba7e98
commit
628e6aff7f
2 changed files with 38 additions and 17 deletions
|
|
@ -249,6 +249,7 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
|
|||
var meta = [];
|
||||
if (page) meta.push('page ' + page);
|
||||
if (s.source_type === 'multimodal_page') meta.push('visual PDF page');
|
||||
if (s.visual_caption_source) meta.push('caption: ' + s.visual_caption_source);
|
||||
if (s.visual_kind || s.source_priority) meta.push(s.visual_kind || s.source_priority);
|
||||
if (s.category) meta.push(s.category);
|
||||
if (s.doc_type || s.type) meta.push(s.doc_type || s.type);
|
||||
|
|
@ -267,6 +268,7 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
|
|||
var badges = [];
|
||||
if (source.source_priority) badges.push(source.source_priority);
|
||||
if (source.visual_kind && badges.indexOf(source.visual_kind) === -1) badges.push(source.visual_kind);
|
||||
if (source.visual_caption_source && badges.indexOf(source.visual_caption_source) === -1) badges.push(source.visual_caption_source);
|
||||
if (source.category) badges.push(source.category);
|
||||
if (source.source_boost && Number(source.source_boost) !== 1) badges.push(Number(source.source_boost).toFixed(2) + 'x');
|
||||
if (!badges.length) return '';
|
||||
|
|
|
|||
|
|
@ -686,6 +686,12 @@ function normalizeMcpMultimodalResponse(result) {
|
|||
if (!data || !Array.isArray(data.results)) return [];
|
||||
return data.results.map(function(r, idx) {
|
||||
var nearby = clip(cleanSourceExcerpt(r.nearby_text || ''), 1400);
|
||||
var caption = clip(cleanSourceExcerpt(r.visual_caption || ''), 900);
|
||||
var labels = Array.isArray(r.visual_labels) ? r.visual_labels.filter(Boolean).slice(0, 20) : [];
|
||||
var excerptParts = [];
|
||||
if (caption) excerptParts.push('[Visual caption] ' + caption);
|
||||
if (labels.length) excerptParts.push('[Visual labels] ' + labels.join(', '));
|
||||
if (nearby) excerptParts.push('[Page text] ' + nearby);
|
||||
return {
|
||||
number: idx + 1,
|
||||
id: r.id,
|
||||
|
|
@ -698,7 +704,10 @@ function normalizeMcpMultimodalResponse(result) {
|
|||
category_path: r.category_path || '',
|
||||
page: r.page_number || r.pageNumber || null,
|
||||
page_count: r.page_count || r.pageCount || null,
|
||||
excerpt: nearby ? '[Page-image match] ' + nearby : '[Page-image match] Rendered PDF page matched the visual/text query.',
|
||||
visual_caption: caption,
|
||||
visual_labels: labels,
|
||||
visual_caption_source: r.visual_caption_source || '',
|
||||
excerpt: excerptParts.length ? '[Page-image match] ' + excerptParts.join('\n') : '[Page-image match] Rendered PDF page matched the visual/text query.',
|
||||
score: r.score,
|
||||
image_path_internal: r.image_path || '',
|
||||
image_url: multimodalImageUrl(r.image_path),
|
||||
|
|
@ -893,7 +902,7 @@ function visualClassifierLabels() {
|
|||
}
|
||||
|
||||
function visualMetadataScore(query, source) {
|
||||
var haystack = [source.title, source.excerpt, source.file_path, source.category, source.subcategory, source.category_path].filter(Boolean).join(' ');
|
||||
var haystack = [source.title, source.excerpt, source.visual_caption, (source.visual_labels || []).join(' '), source.file_path, source.category, source.subcategory, source.category_path].filter(Boolean).join(' ');
|
||||
var score = 0;
|
||||
var intent = visualIntent(query);
|
||||
if (isRadiologyQuery(query) && /\b(radiology|radiograph|x-?ray|cxr|imaging|ct|mri|ultrasound|film)\b/i.test(haystack)) score += 0.12;
|
||||
|
|
@ -1139,28 +1148,38 @@ async function generateImage(prompt, model) {
|
|||
if (!process.env.LITELLM_API_BASE) throw new Error('LiteLLM is required for image generation');
|
||||
var headers = { 'Content-Type': 'application/json' };
|
||||
if (process.env.LITELLM_API_KEY) headers.Authorization = 'Bearer ' + process.env.LITELLM_API_KEY;
|
||||
var size = imageSizeForPrompt(prompt);
|
||||
var resp = await axios.post(gatewayUrl('/images/generations'), {
|
||||
model: model,
|
||||
prompt: imagePromptForCanvas(prompt, size),
|
||||
size: size
|
||||
}, { headers: headers, timeout: 120000 });
|
||||
var size = process.env.CLINICAL_ASSISTANT_IMAGE_SIZE || 'auto';
|
||||
var resp = await generateImageRequest(model, imagePromptForCanvas(prompt), size, headers).catch(async function(e) {
|
||||
if (!isInvalidImageSizeError(e) || size === '1024x1024') throw e;
|
||||
return generateImageRequest(model, imagePromptForCanvas(prompt), '1024x1024', headers);
|
||||
});
|
||||
var item = resp.data && resp.data.data && resp.data.data[0] ? resp.data.data[0] : {};
|
||||
return { imageUrl: item.url || null, base64: item.b64_json || null, raw: (!item.url && !item.b64_json) ? resp.data : undefined };
|
||||
}
|
||||
|
||||
function imageSizeForPrompt(prompt) {
|
||||
var text = String(prompt || '');
|
||||
if (/\b(flow\s*chart|flowchart|algorithm|pathway|timeline|vertical|stepwise|decision\s*tree|age\s*group|0-21|22-28|29-60)\b/i.test(text)) return '1024x1536';
|
||||
if (/\b(table|matrix|comparison|wide|landscape|side-by-side)\b/i.test(text)) return '1536x1024';
|
||||
return '1024x1024';
|
||||
function generateImageRequest(model, prompt, size, headers) {
|
||||
return axios.post(gatewayUrl('/images/generations'), {
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
size: size
|
||||
}, { headers: headers, timeout: 120000 });
|
||||
}
|
||||
|
||||
function imagePromptForCanvas(prompt, size) {
|
||||
function imagePromptForCanvas(prompt) {
|
||||
var text = String(prompt || '');
|
||||
var guidance = ' Keep all text and boxes fully inside the canvas with generous margins. Use fewer words per box, large readable type, and avoid cropping at edges.';
|
||||
if (size === '1024x1536') guidance += ' Use a vertical portrait layout with top-to-bottom flow and ample spacing between decision nodes.';
|
||||
if (size === '1536x1024') guidance += ' Use a wide landscape layout with columns and ample horizontal spacing.';
|
||||
return String(prompt || '').trim() + guidance;
|
||||
if (/\b(flow\s*chart|flowchart|algorithm|pathway|timeline|vertical|stepwise|decision\s*tree|age\s*group|0-21|22-28|29-60)\b/i.test(text)) {
|
||||
guidance += ' Prefer a tall portrait canvas with top-to-bottom flow and ample spacing between decision nodes.';
|
||||
}
|
||||
if (/\b(table|matrix|comparison|wide|landscape|side-by-side)\b/i.test(text)) {
|
||||
guidance += ' Prefer a wide landscape canvas with columns and ample horizontal spacing.';
|
||||
}
|
||||
return text.trim() + guidance;
|
||||
}
|
||||
|
||||
function isInvalidImageSizeError(e) {
|
||||
var detail = e && e.response && e.response.data ? JSON.stringify(e.response.data) : (e && e.message ? e.message : '');
|
||||
return /invalid size|unsupported size|supported sizes/i.test(detail);
|
||||
}
|
||||
|
||||
async function getSetting(key, fallback) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue