feat: port holistic node refinement contract

This commit is contained in:
“BeeRad”
2026-04-11 21:37:52 +10:00
parent 35f9ecf89c
commit 3ae46245ec
119 changed files with 6596 additions and 10982 deletions
+83 -104
View File
@@ -7,11 +7,6 @@ import { getInternalApiBaseUrl } from '@/services/runtime/apiBase';
import { formatNodeForChat } from '../infrastructure/nodeFormatter';
import { validateExplicitDescription } from '@/services/database/quality';
interface ExistingDimension {
name: string;
description: string | null;
}
function ensureNodeDescription(candidate: string | undefined, fallbackLead: string): string {
const normalizedCandidate = typeof candidate === 'string'
? candidate.trim().replace(/\s+/g, ' ')
@@ -27,53 +22,13 @@ function ensureNodeDescription(candidate: string | undefined, fallbackLead: stri
return joined.slice(0, 500);
}
async function fetchExistingDimensions(): Promise<ExistingDimension[]> {
// AI-powered content analysis
async function analyzeContentWithAI(
title: string,
description: string,
contentType: string
) {
try {
const response = await fetch(`${getInternalApiBaseUrl()}/api/dimensions/popular`);
if (!response.ok) return [];
const result = await response.json();
if (!Array.isArray(result.data)) return [];
return result.data
.map((dimension: { dimension?: unknown; description?: unknown }) => ({
name: typeof dimension.dimension === 'string' ? dimension.dimension.trim() : '',
description: typeof dimension.description === 'string' ? dimension.description.trim() : null
}))
.filter((dimension: ExistingDimension) => dimension.name.length > 0);
} catch (error) {
console.warn('YouTube dimension fetch fallback (no dimension context):', error);
return [];
}
}
function selectExistingDimensions(selected: unknown, existingDimensions: ExistingDimension[], max = 5): string[] {
if (!Array.isArray(selected) || existingDimensions.length === 0) return [];
const byLowerName = new Map(existingDimensions.map((dimension) => [dimension.name.toLowerCase(), dimension.name]));
const normalized: string[] = [];
const seen = new Set<string>();
for (const value of selected) {
if (typeof value !== 'string') continue;
const matched = byLowerName.get(value.trim().toLowerCase());
if (!matched) continue;
const key = matched.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
normalized.push(matched);
if (normalized.length >= max) break;
}
return normalized;
}
async function analyzeContentWithAI(title: string, description: string, contentType: string, existingDimensions: ExistingDimension[]) {
try {
const availableDimensionsBlock = existingDimensions.length > 0
? existingDimensions.map((dimension) => `- ${dimension.name}${dimension.description ? `: ${dimension.description}` : ''}`).join('\n')
: '- No existing dimensions available. Return an empty dimensions array.';
const prompt = `Analyze this ${contentType} content and provide classification.
Title: "${title}"
@@ -83,24 +38,25 @@ CRITICAL — nodeDescription rules (max 500 chars):
1. Write natural prose, not labels or a checklist.
2. Make clear what this literally is: "Podcast episode where…", "Talk by…", "Interview with…", "Video essay on…"
3. Name people by their role: the channel/host is the creator, anyone in the title is likely the guest or subject.
4. State the actual claim or thesis from the title.
4. State the actual claim or thesis from the title — don't paraphrase into vague abstractions.
5. Make clear why it belongs in the graph. If that cannot be inferred, say so naturally.
6. Make the workflow status clear. If unknown, say naturally that it has not been reviewed yet.
7. ABSOLUTELY FORBIDDEN: "discusses", "explores", "examines", "talks about", "delves into", "emphasizing the need for", "insightful for understanding", "relevant to".
7. ABSOLUTELY FORBIDDEN: "discusses", "explores", "examines", "talks about", "delves into", "emphasizing the need for", "insightful for understanding", "relevant to". State things directly.
DIMENSION SELECTION:
You must select 0-3 dimensions from the list below.
Do NOT invent new dimension names.
Examples:
- Title: "Dario Amodei — We are near the end of the exponential" / Channel: Dwarkesh Patel
GOOD: "Dwarkesh Patel interview with Anthropic CEO Dario Amodei — argues we're nearing the end of exponential AI scaling. It was added via extraction and the exact reason it belongs in the graph is not yet inferred from the available context, and it has not been reviewed yet."
BAD: "By Dario Amodei — discusses reaching the limits of exponential growth in AI, emphasizing the need for a critical perspective."
Available dimensions:
${availableDimensionsBlock}
- Title: "The spell of language models" / Channel: Andrej Karpathy
GOOD: "Karpathy talk on how LLMs work under the hood — tokenization, attention, and why they feel like magic but aren't. It was added via extraction and the exact reason it belongs in the graph is not yet inferred from the available context, and it has not been reviewed yet."
BAD: "By Andrej Karpathy — explores the nature of language models and their capabilities."
Respond with ONLY valid JSON:
Respond with ONLY valid JSON (no markdown, no code blocks):
{
"enhancedDescription": "A comprehensive summary (3-6 paragraphs, 800-1500 chars).",
"nodeDescription": "<your natural description>",
"dimensions": ["existing-dimension-1"],
"reasoning": "Brief explanation"
"enhancedDescription": "A comprehensive summary (3-6 paragraphs, 800-1500 chars). Cover key points, arguments, takeaways.",
"nodeDescription": "<your natural description following the rules above>",
"reasoning": "Brief explanation of classification choices"
}`;
const response = await generateText({
@@ -109,21 +65,24 @@ Respond with ONLY valid JSON:
maxOutputTokens: 800
});
const content = (response.text || '{}').replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
let content = response.text || '{}';
// Clean up the response - remove markdown code blocks if present
content = content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
const result = JSON.parse(content);
return {
enhancedDescription: result.enhancedDescription || description,
nodeDescription: typeof result.nodeDescription === 'string' ? result.nodeDescription.slice(0, 500) : undefined,
dimensions: selectExistingDimensions(result.dimensions, existingDimensions, 5),
reasoning: result.reasoning || 'AI analysis completed'
};
} catch (error) {
console.warn('YouTube analysis fallback (using default description):', error);
const message = error instanceof Error ? error.message : 'unknown error';
console.warn('YouTube analysis fallback (using default description):', message);
return {
enhancedDescription: description,
nodeDescription: undefined,
dimensions: [],
reasoning: 'Fallback description used'
};
}
@@ -134,21 +93,29 @@ async function summariseTranscript(title: string, transcript: string): Promise<s
return null;
}
const excerpt = transcript.trim().length > 16000
? `${transcript.trim().slice(0, 8000)}\n[...]\n${transcript.trim().slice(-8000)}`
: transcript.trim();
// Limit transcript length to keep token costs manageable
const MAX_CHARS = 16000;
let excerpt = transcript.trim();
if (excerpt.length > MAX_CHARS) {
const head = excerpt.slice(0, MAX_CHARS / 2);
const tail = excerpt.slice(-MAX_CHARS / 2);
excerpt = `${head}\n[...]\n${tail}`;
}
try {
const response = await generateText({
model: openai('gpt-4o-mini'),
prompt: `You are summarising a long-form recording for a knowledge graph entry. Title: "${title}".
const prompt = `You are summarising a long-form recording for a knowledge graph entry. Title: "${title}".
Using the transcript excerpt below, write a concise 3-4 sentence summary covering the main themes, notable claims, and outcomes. Keep the tone factual.
Using the transcript excerpt below, write a concise 3-4 sentence summary covering the main themes, notable claims, and outcomes. If specific terms, frameworks, or memorable lines appear, mention them. Keep the tone factual (no marketing language). If the excerpt appears truncated, note that the summary is based on the portion provided.
Transcript excerpt:
"""
${excerpt}
"""`,
"""
`;
try {
const response = await generateText({
model: openai('gpt-4o-mini'),
prompt,
maxOutputTokens: 400
});
return response.text?.trim() || null;
@@ -162,18 +129,23 @@ export const youtubeExtractTool = tool({
description: 'Extract a YouTube transcript and metadata, create a node, and return summary details',
inputSchema: z.object({
url: z.string().describe('The YouTube video URL to add to knowledge base'),
title: z.string().optional().describe('Custom title (auto-generated if not provided)'),
dimensions: z.array(z.string()).min(1).max(5).optional().describe('Dimension tags to apply to the created node')
title: z.string().optional().describe('Custom title (auto-generated if not provided)')
}),
execute: async ({ url, title, dimensions }) => {
execute: async ({ url, title }) => {
console.log('🎯 YouTubeExtract tool called with URL:', url);
try {
// Validate YouTube URL
if (!url.includes('youtube.com') && !url.includes('youtu.be')) {
return { success: false, error: 'Invalid YouTube URL format', data: null };
return {
success: false,
error: 'Invalid YouTube URL format',
data: null
};
}
let result: { success: boolean; source?: string; metadata?: any; error?: string };
console.log('📝 Using TypeScript yt-dlp extractor');
try {
const extractionResult = await extractYouTube(url);
result = {
@@ -193,46 +165,48 @@ export const youtubeExtractTool = tool({
error: extractionResult.error
};
} catch (error: any) {
result = { success: false, error: error.message || 'TypeScript extraction failed' };
result = {
success: false,
error: error.message || 'TypeScript extraction failed'
};
}
if (!result.success || !result.source) {
return { success: false, error: result.error || 'Failed to extract YouTube content', data: null };
return {
success: false,
error: result.error || 'Failed to extract YouTube content',
data: null
};
}
const existingDimensions = await fetchExistingDimensions();
console.log('🎯 YouTube extraction successful, analyzing with AI...');
// Step 2: AI Analysis for enhanced metadata
const aiAnalysis = await analyzeContentWithAI(
result.metadata?.video_title || 'YouTube Video',
`Video by ${result.metadata?.channel_name || 'Unknown Channel'}`,
'youtube',
existingDimensions
'youtube'
);
// Step 3: Create node with extracted content and AI analysis
const nodeTitle = title || result.metadata?.video_title || `YouTube Video ${url.split('/').pop()?.split('?')[0]}`;
const transcriptSummary = await summariseTranscript(nodeTitle, result.source);
const suppliedDimensions = Array.isArray(dimensions) ? dimensions : [];
const finalDimensions = suppliedDimensions.slice(0, 5).length > 0
? suppliedDimensions.slice(0, 5)
: (aiAnalysis?.dimensions || []).slice(0, 5);
const nodeDescription = ensureNodeDescription(
aiAnalysis?.nodeDescription,
transcriptSummary || `YouTube video by ${result.metadata?.channel_name || 'an unknown creator'} about ${nodeTitle}`
);
const fallbackDescriptionLead = `YouTube video from ${result.metadata?.channel_name || 'an unknown channel'} titled "${nodeTitle}"`;
const finalDescription = ensureNodeDescription(aiAnalysis?.nodeDescription, fallbackDescriptionLead);
const createResponse = await fetch(`${getInternalApiBaseUrl()}/api/nodes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: nodeTitle,
description: nodeDescription,
description: finalDescription,
source: result.source,
link: url,
dimensions: finalDimensions,
metadata: {
type: 'youtube',
state: 'not_processed',
captured_method: 'youtube_extract',
captured_by: 'agent',
captured_by: 'human',
source_metadata: {
video_id: result.metadata?.video_id,
channel_name: result.metadata?.channel_name,
@@ -242,33 +216,38 @@ export const youtubeExtractTool = tool({
total_segments: result.metadata?.total_segments,
language: result.metadata?.language,
extraction_method: result.metadata?.extraction_method,
ai_analysis: aiAnalysis?.reasoning,
summary_origin: transcriptSummary ? 'transcript_summary' : 'metadata_description',
refined_at: new Date().toISOString()
transcript_summary: transcriptSummary,
refined_at: new Date().toISOString(),
}
}
})
});
const createResult = await createResponse.json();
if (!createResponse.ok) {
return { success: false, error: createResult.error || 'Failed to create item', data: null };
return {
success: false,
error: createResult.error || 'Failed to create item',
data: null
};
}
const actualDimensions: string[] = createResult.data?.dimensions || finalDimensions || [];
console.log('🎯 YouTubeExtract completed successfully');
const formattedNode = createResult.data?.id
? formatNodeForChat({ id: createResult.data.id, title: nodeTitle, dimensions: actualDimensions })
? formatNodeForChat({ id: createResult.data.id, title: nodeTitle })
: nodeTitle;
return {
success: true,
message: `Added ${formattedNode} with dimensions: ${actualDimensions.length > 0 ? actualDimensions.join(', ') : 'none'}`,
message: `Added ${formattedNode}`,
data: {
nodeId: createResult.data?.id,
title: nodeTitle,
contentLength: result.source.length,
url,
dimensions: actualDimensions
url: url
}
};
} catch (error) {