feat(journaling): implement secondary LLM validation for YouTube video selection
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m2s
Monorepo Pipeline / 🏗️ Build (push) Successful in 5m24s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m2s
Monorepo Pipeline / 🏗️ Build (push) Successful in 5m24s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
@@ -204,7 +204,7 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
|
|||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: `CONTEXT: ${topic}`,
|
content: `CONTEXT: ${topic}`,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
response_format: { type: "json_object" },
|
response_format: { type: "json_object" },
|
||||||
});
|
});
|
||||||
@@ -226,9 +226,15 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter for youtube results
|
// Filter for youtube results
|
||||||
const ytVideos = videos.filter(
|
const ytVideos = videos
|
||||||
(v) => v.link && v.link.includes("youtube.com/watch"),
|
.filter(
|
||||||
);
|
(v) =>
|
||||||
|
v.link &&
|
||||||
|
v.link.includes("youtube.com/watch") &&
|
||||||
|
v.title &&
|
||||||
|
v.channel,
|
||||||
|
)
|
||||||
|
.slice(0, 5); // Take top 5 for evaluation
|
||||||
|
|
||||||
if (ytVideos.length === 0) {
|
if (ytVideos.length === 0) {
|
||||||
console.warn(`⚠️ [Serper] No YouTube videos in search results.`);
|
console.warn(`⚠️ [Serper] No YouTube videos in search results.`);
|
||||||
@@ -236,8 +242,46 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick the best one (usually the first result)
|
// Step 3: Ask the LLM to evaluate the relevance of the found videos
|
||||||
const bestVideo = ytVideos[0];
|
const evalPrompt = `You are a strict technical evaluator. You must select the MOST RELEVANT educational tech video from the list below based on this context: "${topic.slice(0, 500)}..."
|
||||||
|
|
||||||
|
Videos:
|
||||||
|
${ytVideos.map((v, i) => `[ID: ${i}] Title: "${v.title}" | Channel: "${v.channel}" | Snippet: "${v.snippet || "none"}"`).join("\n")}
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
1. The video MUST be highly relevant to the context.
|
||||||
|
2. The channel SHOULD be a tech, development, or professional business channel (avoid gaming, vlogs, unrelated topics).
|
||||||
|
3. If none are truly relevant, return -1.
|
||||||
|
4. If one is highly relevant, return its ID number.
|
||||||
|
|
||||||
|
Return ONLY a JSON object: {"bestVideoId": number}`;
|
||||||
|
|
||||||
|
const evalResponse = await this.openai.chat.completions.create({
|
||||||
|
model: "google/gemini-2.5-flash",
|
||||||
|
messages: [{ role: "system", content: evalPrompt }],
|
||||||
|
response_format: { type: "json_object" },
|
||||||
|
});
|
||||||
|
|
||||||
|
let bestIdx = -1;
|
||||||
|
try {
|
||||||
|
const evalParsed = JSON.parse(
|
||||||
|
evalResponse.choices[0].message.content || '{"bestVideoId": -1}',
|
||||||
|
);
|
||||||
|
bestIdx = evalParsed.bestVideoId;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse video evaluation response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestIdx < 0 || bestIdx >= ytVideos.length) {
|
||||||
|
console.warn(`⚠️ [Serper] LLM rejected all videos as irrelevant.`);
|
||||||
|
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bestVideo = ytVideos[bestIdx];
|
||||||
|
console.log(
|
||||||
|
`✅ [Serper] AI selected video: ${bestVideo.title} (Channel: ${bestVideo.channel})`,
|
||||||
|
);
|
||||||
|
|
||||||
// Extract the 11-char video ID from the link (e.g., https://www.youtube.com/watch?v=dQw4w9WgXcQ)
|
// Extract the 11-char video ID from the link (e.g., https://www.youtube.com/watch?v=dQw4w9WgXcQ)
|
||||||
const urlObj = new URL(bestVideo.link);
|
const urlObj = new URL(bestVideo.link);
|
||||||
@@ -299,7 +343,7 @@ CRITICAL: Do NOT provide more than 2 trendsKeywords. Keep it extremely focused.`
|
|||||||
try {
|
try {
|
||||||
let parsed = JSON.parse(
|
let parsed = JSON.parse(
|
||||||
response.choices[0].message.content ||
|
response.choices[0].message.content ||
|
||||||
'{"trendsKeywords": [], "dcVariables": []}',
|
'{"trendsKeywords": [], "dcVariables": []}',
|
||||||
);
|
);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };
|
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };
|
||||||
|
|||||||
Reference in New Issue
Block a user