mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(core): add language lesson prompt builder for contextual code teaching
Add detectLanguageConcepts, buildLanguageLessonPrompt, and parseLanguageLessonResponse to support language-specific lessons tied to individual graph nodes. Includes 12 concept detection patterns (async/await, generics, middleware, etc.) and structured LLM prompt generation with JSON response parsing. 10 new tests, 104 total passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8747ed30b4
commit
3b298c1a7a
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
buildLanguageLessonPrompt,
|
||||
parseLanguageLessonResponse,
|
||||
detectLanguageConcepts,
|
||||
} from "../analyzer/language-lesson.js";
|
||||
import type { GraphNode, GraphEdge } from "../types.js";
|
||||
|
||||
const sampleNode: GraphNode = {
|
||||
id: "func:auth:verifyToken",
|
||||
type: "function",
|
||||
name: "verifyToken",
|
||||
filePath: "src/auth/verify.ts",
|
||||
lineRange: [10, 35],
|
||||
summary: "Verifies JWT tokens and extracts user payload using async/await",
|
||||
tags: ["auth", "jwt", "async"],
|
||||
complexity: "moderate",
|
||||
};
|
||||
|
||||
const sampleEdges: GraphEdge[] = [
|
||||
{
|
||||
source: "func:auth:verifyToken",
|
||||
target: "file:src/config.ts",
|
||||
type: "reads_from",
|
||||
direction: "forward",
|
||||
weight: 0.6,
|
||||
},
|
||||
{
|
||||
source: "file:src/middleware.ts",
|
||||
target: "func:auth:verifyToken",
|
||||
type: "calls",
|
||||
direction: "forward",
|
||||
weight: 0.8,
|
||||
},
|
||||
];
|
||||
|
||||
describe("language-lesson", () => {
|
||||
describe("buildLanguageLessonPrompt", () => {
|
||||
it("includes the node name and summary", () => {
|
||||
const prompt = buildLanguageLessonPrompt(
|
||||
sampleNode,
|
||||
sampleEdges,
|
||||
"typescript",
|
||||
);
|
||||
expect(prompt).toContain("verifyToken");
|
||||
expect(prompt).toContain("JWT tokens");
|
||||
});
|
||||
|
||||
it("includes the target language", () => {
|
||||
const prompt = buildLanguageLessonPrompt(
|
||||
sampleNode,
|
||||
sampleEdges,
|
||||
"typescript",
|
||||
);
|
||||
expect(prompt).toContain("TypeScript");
|
||||
});
|
||||
|
||||
it("includes relationship context", () => {
|
||||
const prompt = buildLanguageLessonPrompt(
|
||||
sampleNode,
|
||||
sampleEdges,
|
||||
"typescript",
|
||||
);
|
||||
expect(prompt).toContain("reads_from");
|
||||
});
|
||||
|
||||
it("requests JSON output", () => {
|
||||
const prompt = buildLanguageLessonPrompt(
|
||||
sampleNode,
|
||||
sampleEdges,
|
||||
"typescript",
|
||||
);
|
||||
expect(prompt).toContain("JSON");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseLanguageLessonResponse", () => {
|
||||
it("parses a valid response", () => {
|
||||
const response = JSON.stringify({
|
||||
languageNotes:
|
||||
"Uses async/await for non-blocking token verification.",
|
||||
concepts: [
|
||||
{
|
||||
name: "async/await",
|
||||
explanation:
|
||||
"The function uses async/await to handle asynchronous JWT verification.",
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = parseLanguageLessonResponse(response);
|
||||
expect(result.languageNotes).toBe(
|
||||
"Uses async/await for non-blocking token verification.",
|
||||
);
|
||||
expect(result.concepts).toHaveLength(1);
|
||||
expect(result.concepts[0].name).toBe("async/await");
|
||||
expect(result.concepts[0].explanation).toContain("async/await");
|
||||
});
|
||||
|
||||
it("extracts JSON from code blocks", () => {
|
||||
const response = `Here is the analysis:
|
||||
\`\`\`json
|
||||
{
|
||||
"languageNotes": "TypeScript generics used here.",
|
||||
"concepts": [
|
||||
{ "name": "generics", "explanation": "Type parameters enable reuse." }
|
||||
]
|
||||
}
|
||||
\`\`\``;
|
||||
const result = parseLanguageLessonResponse(response);
|
||||
expect(result.languageNotes).toBe("TypeScript generics used here.");
|
||||
expect(result.concepts).toHaveLength(1);
|
||||
expect(result.concepts[0].name).toBe("generics");
|
||||
});
|
||||
|
||||
it("returns empty result for invalid response", () => {
|
||||
const result = parseLanguageLessonResponse("");
|
||||
expect(result).toEqual({ languageNotes: "", concepts: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectLanguageConcepts", () => {
|
||||
it("detects async patterns from tags", () => {
|
||||
const concepts = detectLanguageConcepts(sampleNode, "typescript");
|
||||
expect(concepts).toContain("async/await");
|
||||
});
|
||||
|
||||
it("detects middleware pattern", () => {
|
||||
const middlewareNode: GraphNode = {
|
||||
id: "func:middleware:auth",
|
||||
type: "function",
|
||||
name: "authMiddleware",
|
||||
filePath: "src/middleware/auth.ts",
|
||||
summary: "Express middleware for authentication",
|
||||
tags: ["middleware", "auth"],
|
||||
complexity: "moderate",
|
||||
};
|
||||
const concepts = detectLanguageConcepts(middlewareNode, "typescript");
|
||||
expect(concepts).toContain("middleware pattern");
|
||||
});
|
||||
|
||||
it("returns empty for nodes with no detectable concepts", () => {
|
||||
const plainNode: GraphNode = {
|
||||
id: "file:src/config.ts",
|
||||
type: "file",
|
||||
name: "config.ts",
|
||||
filePath: "src/config.ts",
|
||||
summary: "Exports configuration values from environment variables",
|
||||
tags: ["config"],
|
||||
complexity: "simple",
|
||||
};
|
||||
const concepts = detectLanguageConcepts(plainNode, "typescript");
|
||||
expect(concepts).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import type { GraphNode, GraphEdge } from "../types.js";
|
||||
|
||||
export interface LanguageLessonResult {
|
||||
languageNotes: string;
|
||||
concepts: Array<{ name: string; explanation: string }>;
|
||||
}
|
||||
|
||||
const CONCEPT_PATTERNS: Record<string, string[]> = {
|
||||
"async/await": ["async", "await", "promise", "asynchronous"],
|
||||
"middleware pattern": ["middleware", "interceptor", "pipe"],
|
||||
"generics": ["generic", "type parameter", "template"],
|
||||
"decorators": ["decorator", "@", "annotation"],
|
||||
"dependency injection": ["inject", "provider", "container", "di"],
|
||||
"observer pattern": [
|
||||
"subscribe",
|
||||
"publish",
|
||||
"event",
|
||||
"observable",
|
||||
"listener",
|
||||
],
|
||||
"singleton": ["singleton", "instance", "shared client"],
|
||||
"type guards": ["type guard", "is", "narrowing", "discriminated union"],
|
||||
"higher-order functions": [
|
||||
"callback",
|
||||
"factory",
|
||||
"higher-order",
|
||||
"closure",
|
||||
],
|
||||
"error handling": [
|
||||
"try/catch",
|
||||
"error boundary",
|
||||
"exception",
|
||||
"Result type",
|
||||
],
|
||||
"streams": ["stream", "pipe", "transform", "readable", "writable"],
|
||||
"concurrency": ["goroutine", "channel", "thread", "worker", "mutex"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects language concepts present in a graph node based on its tags, summary, and languageNotes.
|
||||
*/
|
||||
export function detectLanguageConcepts(
|
||||
node: GraphNode,
|
||||
language: string,
|
||||
): string[] {
|
||||
const text = [
|
||||
...node.tags,
|
||||
node.summary.toLowerCase(),
|
||||
node.languageNotes?.toLowerCase() ?? "",
|
||||
].join(" ");
|
||||
|
||||
const detected: string[] = [];
|
||||
|
||||
for (const [concept, keywords] of Object.entries(CONCEPT_PATTERNS)) {
|
||||
const found = keywords.some((keyword) =>
|
||||
text.toLowerCase().includes(keyword.toLowerCase()),
|
||||
);
|
||||
if (found) {
|
||||
detected.push(concept);
|
||||
}
|
||||
}
|
||||
|
||||
return detected;
|
||||
}
|
||||
|
||||
const LANGUAGE_DISPLAY_NAMES: Record<string, string> = {
|
||||
typescript: "TypeScript",
|
||||
javascript: "JavaScript",
|
||||
coffeescript: "CoffeeScript",
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a prompt that asks an LLM to produce a language-specific lesson for a given node.
|
||||
*/
|
||||
export function buildLanguageLessonPrompt(
|
||||
node: GraphNode,
|
||||
edges: GraphEdge[],
|
||||
language: string,
|
||||
): string {
|
||||
const capitalizedLanguage =
|
||||
LANGUAGE_DISPLAY_NAMES[language.toLowerCase()] ??
|
||||
language.charAt(0).toUpperCase() + language.slice(1);
|
||||
|
||||
const concepts = detectLanguageConcepts(node, language);
|
||||
|
||||
const relationships = edges
|
||||
.map((edge) => {
|
||||
const arrow = edge.direction === "forward" ? "->" : "<-";
|
||||
const other =
|
||||
edge.source === node.id ? edge.target : edge.source;
|
||||
return ` ${arrow} ${edge.type} ${other}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const conceptSection =
|
||||
concepts.length > 0
|
||||
? `\nDetected concepts to explain:\n${concepts.map((c) => ` - ${c}`).join("\n")}`
|
||||
: `\nNo specific concepts were pre-detected. Please identify any ${capitalizedLanguage} patterns or idioms present.`;
|
||||
|
||||
return `You are a programming teacher specializing in ${capitalizedLanguage}. Analyze the following code component and create a language-specific lesson.
|
||||
|
||||
Component: ${node.name}
|
||||
Type: ${node.type}
|
||||
File: ${node.filePath ?? "N/A"}
|
||||
Summary: ${node.summary}
|
||||
Tags: ${node.tags.join(", ")}
|
||||
|
||||
Relationships:
|
||||
${relationships}
|
||||
${conceptSection}
|
||||
|
||||
Return a JSON object with the following fields:
|
||||
- "languageNotes": A concise explanation of the ${capitalizedLanguage}-specific patterns and idioms used in this component.
|
||||
- "concepts": An array of objects, each with:
|
||||
- "name": The concept name (e.g., "async/await", "generics").
|
||||
- "explanation": A beginner-friendly explanation of this concept as it applies to this component.
|
||||
|
||||
Respond ONLY with the JSON object, no additional text.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a JSON block from an LLM response, handling markdown fences.
|
||||
*/
|
||||
function extractJson(response: string): string {
|
||||
const fenceMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
||||
if (fenceMatch) {
|
||||
return fenceMatch[1].trim();
|
||||
}
|
||||
|
||||
const objectMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (objectMatch) {
|
||||
return objectMatch[0].trim();
|
||||
}
|
||||
|
||||
return response.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an LLM response for language lesson content.
|
||||
* Returns a safe default on parse failure.
|
||||
*/
|
||||
export function parseLanguageLessonResponse(
|
||||
response: string,
|
||||
): LanguageLessonResult {
|
||||
try {
|
||||
const jsonStr = extractJson(response);
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
|
||||
const languageNotes =
|
||||
typeof parsed.languageNotes === "string" ? parsed.languageNotes : "";
|
||||
|
||||
const concepts = Array.isArray(parsed.concepts)
|
||||
? parsed.concepts
|
||||
.filter(
|
||||
(
|
||||
c: unknown,
|
||||
): c is { name: string; explanation: string } =>
|
||||
typeof c === "object" &&
|
||||
c !== null &&
|
||||
typeof (c as Record<string, unknown>).name === "string" &&
|
||||
typeof (c as Record<string, unknown>).explanation ===
|
||||
"string",
|
||||
)
|
||||
.map((c: { name: string; explanation: string }) => ({
|
||||
name: c.name,
|
||||
explanation: c.explanation,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return { languageNotes, concepts };
|
||||
} catch {
|
||||
return { languageNotes: "", concepts: [] };
|
||||
}
|
||||
}
|
||||
@@ -29,3 +29,9 @@ export {
|
||||
parseTourGenerationResponse,
|
||||
generateHeuristicTour,
|
||||
} from "./analyzer/tour-generator.js";
|
||||
export {
|
||||
buildLanguageLessonPrompt,
|
||||
parseLanguageLessonResponse,
|
||||
detectLanguageConcepts,
|
||||
type LanguageLessonResult,
|
||||
} from "./analyzer/language-lesson.js";
|
||||
|
||||
Reference in New Issue
Block a user