mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(core): add tour generation engine with LLM and heuristic strategies
Add three exported functions for generating guided codebase tours: - buildTourGenerationPrompt: builds LLM prompt with project metadata - parseTourGenerationResponse: parses LLM JSON response with validation - generateHeuristicTour: topology-based tour using Kahn's algorithm, with layer grouping and concept node separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
buildTourGenerationPrompt,
|
||||
parseTourGenerationResponse,
|
||||
generateHeuristicTour,
|
||||
} from "../analyzer/tour-generator.js";
|
||||
import type { KnowledgeGraph } from "../types.js";
|
||||
|
||||
const sampleGraph: KnowledgeGraph = {
|
||||
version: "1.0.0",
|
||||
project: {
|
||||
name: "test-project",
|
||||
languages: ["typescript"],
|
||||
frameworks: ["express"],
|
||||
description: "A test project",
|
||||
analyzedAt: "2026-03-14T00:00:00Z",
|
||||
gitCommitHash: "abc123",
|
||||
},
|
||||
nodes: [
|
||||
{ id: "file:src/index.ts", type: "file", name: "index.ts", filePath: "src/index.ts", summary: "Application entry point", tags: ["entry", "server"], complexity: "simple" },
|
||||
{ id: "file:src/routes.ts", type: "file", name: "routes.ts", filePath: "src/routes.ts", summary: "Route definitions", tags: ["routes", "api"], complexity: "moderate" },
|
||||
{ id: "file:src/service.ts", type: "file", name: "service.ts", filePath: "src/service.ts", summary: "Business logic", tags: ["service"], complexity: "complex" },
|
||||
{ id: "file:src/db.ts", type: "file", name: "db.ts", filePath: "src/db.ts", summary: "Database connection", tags: ["database"], complexity: "simple" },
|
||||
{ id: "concept:auth-flow", type: "concept", name: "Auth Flow", summary: "Authentication concept", tags: ["concept", "auth"], complexity: "moderate" },
|
||||
],
|
||||
edges: [
|
||||
{ source: "file:src/index.ts", target: "file:src/routes.ts", type: "imports", direction: "forward", weight: 0.9 },
|
||||
{ source: "file:src/routes.ts", target: "file:src/service.ts", type: "calls", direction: "forward", weight: 0.8 },
|
||||
{ source: "file:src/service.ts", target: "file:src/db.ts", type: "reads_from", direction: "forward", weight: 0.7 },
|
||||
],
|
||||
layers: [
|
||||
{ id: "layer:api", name: "API Layer", description: "HTTP routes", nodeIds: ["file:src/index.ts", "file:src/routes.ts"] },
|
||||
{ id: "layer:service", name: "Service Layer", description: "Business logic", nodeIds: ["file:src/service.ts"] },
|
||||
{ id: "layer:data", name: "Data Layer", description: "Database", nodeIds: ["file:src/db.ts"] },
|
||||
],
|
||||
tour: [],
|
||||
};
|
||||
|
||||
describe("tour-generator", () => {
|
||||
describe("buildTourGenerationPrompt", () => {
|
||||
it("includes project name and description", () => {
|
||||
const prompt = buildTourGenerationPrompt(sampleGraph);
|
||||
expect(prompt).toContain("test-project");
|
||||
expect(prompt).toContain("A test project");
|
||||
});
|
||||
|
||||
it("includes all node summaries", () => {
|
||||
const prompt = buildTourGenerationPrompt(sampleGraph);
|
||||
expect(prompt).toContain("Application entry point");
|
||||
expect(prompt).toContain("Route definitions");
|
||||
expect(prompt).toContain("Business logic");
|
||||
expect(prompt).toContain("Database connection");
|
||||
expect(prompt).toContain("Authentication concept");
|
||||
});
|
||||
|
||||
it("includes layer information", () => {
|
||||
const prompt = buildTourGenerationPrompt(sampleGraph);
|
||||
expect(prompt).toContain("API Layer");
|
||||
expect(prompt).toContain("Service Layer");
|
||||
expect(prompt).toContain("Data Layer");
|
||||
});
|
||||
|
||||
it("requests JSON output format", () => {
|
||||
const prompt = buildTourGenerationPrompt(sampleGraph);
|
||||
expect(prompt).toContain("JSON");
|
||||
expect(prompt).toContain("steps");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTourGenerationResponse", () => {
|
||||
it("parses valid JSON response with tour steps", () => {
|
||||
const response = JSON.stringify({
|
||||
steps: [
|
||||
{
|
||||
order: 1,
|
||||
title: "Entry Point",
|
||||
description: "Start here",
|
||||
nodeIds: ["file:src/index.ts"],
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
title: "Routes",
|
||||
description: "API routes",
|
||||
nodeIds: ["file:src/routes.ts"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const steps = parseTourGenerationResponse(response);
|
||||
expect(steps).toHaveLength(2);
|
||||
expect(steps[0].order).toBe(1);
|
||||
expect(steps[0].title).toBe("Entry Point");
|
||||
expect(steps[0].nodeIds).toEqual(["file:src/index.ts"]);
|
||||
expect(steps[1].order).toBe(2);
|
||||
});
|
||||
|
||||
it("extracts JSON from markdown code blocks", () => {
|
||||
const response = `Here is the tour:
|
||||
\`\`\`json
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"order": 1,
|
||||
"title": "Start",
|
||||
"description": "The beginning",
|
||||
"nodeIds": ["file:src/index.ts"]
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\``;
|
||||
const steps = parseTourGenerationResponse(response);
|
||||
expect(steps).toHaveLength(1);
|
||||
expect(steps[0].title).toBe("Start");
|
||||
});
|
||||
|
||||
it("returns empty array for unparseable response", () => {
|
||||
expect(parseTourGenerationResponse("not json at all")).toEqual([]);
|
||||
expect(parseTourGenerationResponse("")).toEqual([]);
|
||||
expect(parseTourGenerationResponse("random text here")).toEqual([]);
|
||||
});
|
||||
|
||||
it("filters out steps with missing required fields", () => {
|
||||
const response = JSON.stringify({
|
||||
steps: [
|
||||
{
|
||||
order: 1,
|
||||
title: "Valid Step",
|
||||
description: "Has everything",
|
||||
nodeIds: ["file:src/index.ts"],
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
// missing title
|
||||
description: "Missing title",
|
||||
nodeIds: ["file:src/routes.ts"],
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
title: "Missing description",
|
||||
// missing description
|
||||
nodeIds: ["file:src/routes.ts"],
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
title: "Missing nodeIds",
|
||||
description: "No nodes",
|
||||
// missing nodeIds
|
||||
},
|
||||
{
|
||||
// missing order
|
||||
title: "Missing order",
|
||||
description: "No order",
|
||||
nodeIds: ["file:src/db.ts"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const steps = parseTourGenerationResponse(response);
|
||||
expect(steps).toHaveLength(1);
|
||||
expect(steps[0].title).toBe("Valid Step");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateHeuristicTour", () => {
|
||||
it("starts with entry-point nodes", () => {
|
||||
const tour = generateHeuristicTour(sampleGraph);
|
||||
// Entry point node (0 incoming edges) is file:src/index.ts
|
||||
// It should appear in the first step's nodeIds
|
||||
const firstStepNodeIds = tour[0].nodeIds;
|
||||
expect(firstStepNodeIds).toContain("file:src/index.ts");
|
||||
});
|
||||
|
||||
it("follows topological order", () => {
|
||||
const tour = generateHeuristicTour(sampleGraph);
|
||||
// Collect all code node IDs in order across steps (excluding concept steps)
|
||||
const codeSteps = tour.filter(
|
||||
(s) => !s.title.toLowerCase().includes("concept"),
|
||||
);
|
||||
const orderedNodeIds = codeSteps.flatMap((s) => s.nodeIds);
|
||||
|
||||
// index.ts must appear before routes.ts
|
||||
const indexPos = orderedNodeIds.indexOf("file:src/index.ts");
|
||||
const routesPos = orderedNodeIds.indexOf("file:src/routes.ts");
|
||||
const servicePos = orderedNodeIds.indexOf("file:src/service.ts");
|
||||
const dbPos = orderedNodeIds.indexOf("file:src/db.ts");
|
||||
|
||||
expect(indexPos).toBeLessThan(routesPos);
|
||||
expect(routesPos).toBeLessThan(servicePos);
|
||||
expect(servicePos).toBeLessThan(dbPos);
|
||||
});
|
||||
|
||||
it("includes concept nodes in separate steps", () => {
|
||||
const tour = generateHeuristicTour(sampleGraph);
|
||||
// There should be a step containing the concept node
|
||||
const conceptStep = tour.find((s) =>
|
||||
s.nodeIds.includes("concept:auth-flow"),
|
||||
);
|
||||
expect(conceptStep).toBeDefined();
|
||||
// Concept step should not contain file nodes
|
||||
const fileNodeIds = sampleGraph.nodes
|
||||
.filter((n) => n.type === "file")
|
||||
.map((n) => n.id);
|
||||
for (const fileId of fileNodeIds) {
|
||||
expect(conceptStep!.nodeIds).not.toContain(fileId);
|
||||
}
|
||||
});
|
||||
|
||||
it("assigns order numbers sequentially", () => {
|
||||
const tour = generateHeuristicTour(sampleGraph);
|
||||
for (let i = 0; i < tour.length; i++) {
|
||||
expect(tour[i].order).toBe(i + 1);
|
||||
}
|
||||
});
|
||||
|
||||
it("groups nodes by layer when layers exist", () => {
|
||||
const tour = generateHeuristicTour(sampleGraph);
|
||||
// With layers, steps should reference layer names
|
||||
const stepTitles = tour.map((s) => s.title);
|
||||
// Should have steps that reference the layer names
|
||||
const hasApiLayer = stepTitles.some((t) => t.includes("API Layer"));
|
||||
const hasServiceLayer = stepTitles.some((t) => t.includes("Service Layer"));
|
||||
const hasDataLayer = stepTitles.some((t) => t.includes("Data Layer"));
|
||||
expect(hasApiLayer).toBe(true);
|
||||
expect(hasServiceLayer).toBe(true);
|
||||
expect(hasDataLayer).toBe(true);
|
||||
});
|
||||
|
||||
it("produces valid TourStep objects", () => {
|
||||
const tour = generateHeuristicTour(sampleGraph);
|
||||
for (const step of tour) {
|
||||
expect(typeof step.order).toBe("number");
|
||||
expect(typeof step.title).toBe("string");
|
||||
expect(step.title.length).toBeGreaterThan(0);
|
||||
expect(typeof step.description).toBe("string");
|
||||
expect(step.description.length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(step.nodeIds)).toBe(true);
|
||||
expect(step.nodeIds.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles graph with no edges gracefully", () => {
|
||||
const noEdgesGraph: KnowledgeGraph = {
|
||||
...sampleGraph,
|
||||
edges: [],
|
||||
layers: [],
|
||||
};
|
||||
const tour = generateHeuristicTour(noEdgesGraph);
|
||||
expect(tour.length).toBeGreaterThan(0);
|
||||
// All code nodes should still appear somewhere
|
||||
const allNodeIds = tour.flatMap((s) => s.nodeIds);
|
||||
for (const node of noEdgesGraph.nodes) {
|
||||
expect(allNodeIds).toContain(node.id);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles graph with no layers", () => {
|
||||
const noLayersGraph: KnowledgeGraph = {
|
||||
...sampleGraph,
|
||||
layers: [],
|
||||
};
|
||||
const tour = generateHeuristicTour(noLayersGraph);
|
||||
expect(tour.length).toBeGreaterThan(0);
|
||||
// Should batch code nodes (3 per step) instead of grouping by layer
|
||||
const codeSteps = tour.filter(
|
||||
(s) => !s.title.toLowerCase().includes("concept"),
|
||||
);
|
||||
// With 4 code nodes and batches of 3, expect 2 code steps
|
||||
expect(codeSteps.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
import type { KnowledgeGraph, TourStep } from "../types.js";
|
||||
|
||||
/**
|
||||
* Builds an LLM prompt asking for a guided tour of the project.
|
||||
* Includes project metadata, node summaries, edges, and layer info.
|
||||
*/
|
||||
export function buildTourGenerationPrompt(graph: KnowledgeGraph): string {
|
||||
const { project, nodes, edges, layers } = graph;
|
||||
|
||||
const nodeList = nodes
|
||||
.map(
|
||||
(n) =>
|
||||
` - [${n.type}] ${n.name}${n.filePath ? ` (${n.filePath})` : ""}: ${n.summary}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const edgeList = edges
|
||||
.slice(0, 50)
|
||||
.map((e) => ` - ${e.source} --${e.type}--> ${e.target}`)
|
||||
.join("\n");
|
||||
|
||||
const layerList =
|
||||
layers.length > 0
|
||||
? layers
|
||||
.map(
|
||||
(l) =>
|
||||
` - ${l.name}: ${l.description} (nodes: ${l.nodeIds.join(", ")})`,
|
||||
)
|
||||
.join("\n")
|
||||
: " (no layers detected)";
|
||||
|
||||
return `You are a software architecture educator. Generate a guided tour of the following project that helps a newcomer understand the codebase step by step.
|
||||
|
||||
Project: ${project.name}
|
||||
Description: ${project.description}
|
||||
Languages: ${project.languages.join(", ")}
|
||||
Frameworks: ${project.frameworks.join(", ")}
|
||||
|
||||
Nodes:
|
||||
${nodeList}
|
||||
|
||||
Edges (dependencies/relationships):
|
||||
${edgeList}
|
||||
|
||||
Layers:
|
||||
${layerList}
|
||||
|
||||
Create a logical tour that:
|
||||
1. Starts with entry points or high-level overview files
|
||||
2. Follows the natural dependency flow
|
||||
3. Groups related files together
|
||||
4. Ends with supporting utilities or concepts
|
||||
|
||||
Return a JSON object with a "steps" array. Each step must have:
|
||||
- "order": sequential number starting from 1
|
||||
- "title": a short descriptive title for this tour stop
|
||||
- "description": 2-3 sentences explaining what the reader will learn at this step
|
||||
- "nodeIds": array of node IDs to highlight for this step
|
||||
- "languageLesson" (optional): a brief note about language-specific patterns seen in these files
|
||||
|
||||
Respond ONLY with the JSON object, no additional text.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an LLM response for tour generation.
|
||||
* Handles raw JSON and JSON wrapped in markdown code fences.
|
||||
* Filters out steps missing required fields.
|
||||
* Returns empty array if parsing fails.
|
||||
*/
|
||||
export function parseTourGenerationResponse(response: string): TourStep[] {
|
||||
if (!response || response.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to extract from markdown code fences
|
||||
const fenceMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
||||
const jsonStr = fenceMatch ? fenceMatch[1].trim() : response.trim();
|
||||
|
||||
// Try to find a JSON object with steps
|
||||
const objectMatch = jsonStr.match(/\{[\s\S]*\}/);
|
||||
if (!objectMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(objectMatch[0]);
|
||||
|
||||
if (!parsed || !Array.isArray(parsed.steps)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter and validate each step
|
||||
const steps: TourStep[] = [];
|
||||
for (const item of parsed.steps) {
|
||||
if (typeof item !== "object" || item === null) continue;
|
||||
if (typeof item.order !== "number") continue;
|
||||
if (typeof item.title !== "string" || item.title.length === 0) continue;
|
||||
if (typeof item.description !== "string" || item.description.length === 0)
|
||||
continue;
|
||||
if (!Array.isArray(item.nodeIds) || item.nodeIds.length === 0) continue;
|
||||
|
||||
const step: TourStep = {
|
||||
order: item.order,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
nodeIds: item.nodeIds.filter((id: unknown) => typeof id === "string"),
|
||||
};
|
||||
|
||||
if (typeof item.languageLesson === "string") {
|
||||
step.languageLesson = item.languageLesson;
|
||||
}
|
||||
|
||||
steps.push(step);
|
||||
}
|
||||
|
||||
return steps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tour heuristically (without an LLM) using graph topology.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Separate concept nodes from code nodes
|
||||
* 2. Build adjacency info from edges
|
||||
* 3. Find entry points (nodes with 0 incoming edges)
|
||||
* 4. Topological sort (Kahn's algorithm)
|
||||
* 5. If layers exist: group by layer in topological order
|
||||
* 6. If no layers: batch by 3 nodes per step
|
||||
* 7. Add concept nodes as final "Key Concepts" step
|
||||
* 8. Assign sequential order numbers
|
||||
*/
|
||||
export function generateHeuristicTour(graph: KnowledgeGraph): TourStep[] {
|
||||
const { nodes, edges, layers } = graph;
|
||||
|
||||
// Separate concept nodes from code nodes
|
||||
const conceptNodes = nodes.filter((n) => n.type === "concept");
|
||||
const codeNodes = nodes.filter((n) => n.type !== "concept");
|
||||
const codeNodeIds = new Set(codeNodes.map((n) => n.id));
|
||||
|
||||
// Build adjacency info (only for code nodes)
|
||||
const inDegree = new Map<string, number>();
|
||||
const adjacency = new Map<string, string[]>();
|
||||
|
||||
for (const node of codeNodes) {
|
||||
inDegree.set(node.id, 0);
|
||||
adjacency.set(node.id, []);
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
if (!codeNodeIds.has(edge.source) || !codeNodeIds.has(edge.target))
|
||||
continue;
|
||||
inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
|
||||
adjacency.get(edge.source)!.push(edge.target);
|
||||
}
|
||||
|
||||
// Kahn's algorithm for topological sort
|
||||
const queue: string[] = [];
|
||||
for (const [nodeId, degree] of inDegree) {
|
||||
if (degree === 0) {
|
||||
queue.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
const topoOrder: string[] = [];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
topoOrder.push(current);
|
||||
|
||||
for (const neighbor of adjacency.get(current) ?? []) {
|
||||
const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
|
||||
inDegree.set(neighbor, newDegree);
|
||||
if (newDegree === 0) {
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any nodes not reached by topological sort (isolated nodes or cycles)
|
||||
for (const node of codeNodes) {
|
||||
if (!topoOrder.includes(node.id)) {
|
||||
topoOrder.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Build tour steps
|
||||
const steps: TourStep[] = [];
|
||||
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
||||
|
||||
if (layers.length > 0) {
|
||||
// Group by layer in topological order
|
||||
const nodeToLayer = new Map<string, string>();
|
||||
for (const layer of layers) {
|
||||
for (const nodeId of layer.nodeIds) {
|
||||
nodeToLayer.set(nodeId, layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine layer order from topological sort
|
||||
const layerOrder: string[] = [];
|
||||
const layerNodes = new Map<string, string[]>();
|
||||
|
||||
for (const nodeId of topoOrder) {
|
||||
const layerId = nodeToLayer.get(nodeId);
|
||||
if (layerId) {
|
||||
if (!layerNodes.has(layerId)) {
|
||||
layerNodes.set(layerId, []);
|
||||
layerOrder.push(layerId);
|
||||
}
|
||||
layerNodes.get(layerId)!.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Create steps for each layer
|
||||
const layerMap = new Map(layers.map((l) => [l.id, l]));
|
||||
for (const layerId of layerOrder) {
|
||||
const layer = layerMap.get(layerId);
|
||||
const nodeIds = layerNodes.get(layerId) ?? [];
|
||||
if (layer && nodeIds.length > 0) {
|
||||
const nodeSummaries = nodeIds
|
||||
.map((id) => nodeMap.get(id)?.name)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
steps.push({
|
||||
order: 0, // assigned later
|
||||
title: layer.name,
|
||||
description: `${layer.description}. Key files: ${nodeSummaries}.`,
|
||||
nodeIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add unlayered code nodes as "Supporting Components"
|
||||
const layeredNodeIds = new Set(
|
||||
layers.flatMap((l) => l.nodeIds),
|
||||
);
|
||||
const unlayeredNodes = topoOrder.filter(
|
||||
(id) => !layeredNodeIds.has(id),
|
||||
);
|
||||
if (unlayeredNodes.length > 0) {
|
||||
const nodeSummaries = unlayeredNodes
|
||||
.map((id) => nodeMap.get(id)?.name)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
steps.push({
|
||||
order: 0,
|
||||
title: "Supporting Components",
|
||||
description: `Additional supporting files: ${nodeSummaries}.`,
|
||||
nodeIds: unlayeredNodes,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No layers: batch by 3 nodes per step
|
||||
for (let i = 0; i < topoOrder.length; i += 3) {
|
||||
const batch = topoOrder.slice(i, i + 3);
|
||||
const nodeSummaries = batch
|
||||
.map((id) => {
|
||||
const node = nodeMap.get(id);
|
||||
return node ? `${node.name} (${node.summary})` : id;
|
||||
})
|
||||
.join("; ");
|
||||
const stepNumber = Math.floor(i / 3) + 1;
|
||||
steps.push({
|
||||
order: 0, // assigned later
|
||||
title: `Step ${stepNumber}: Code Walkthrough`,
|
||||
description: `Exploring: ${nodeSummaries}.`,
|
||||
nodeIds: batch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add concept nodes as final step if any exist
|
||||
if (conceptNodes.length > 0) {
|
||||
const conceptSummaries = conceptNodes
|
||||
.map((n) => `${n.name} (${n.summary})`)
|
||||
.join("; ");
|
||||
steps.push({
|
||||
order: 0,
|
||||
title: "Key Concepts",
|
||||
description: `Important architectural concepts: ${conceptSummaries}.`,
|
||||
nodeIds: conceptNodes.map((n) => n.id),
|
||||
});
|
||||
}
|
||||
|
||||
// Assign sequential order numbers
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
steps[i].order = i + 1;
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
@@ -24,3 +24,8 @@ export {
|
||||
applyLLMLayers,
|
||||
} from "./analyzer/layer-detector.js";
|
||||
export type { LLMLayerResponse } from "./analyzer/layer-detector.js";
|
||||
export {
|
||||
buildTourGenerationPrompt,
|
||||
parseTourGenerationResponse,
|
||||
generateHeuristicTour,
|
||||
} from "./analyzer/tour-generator.js";
|
||||
|
||||
Reference in New Issue
Block a user