@voicenter-team/nuxt-llms-generator 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4,11 +4,199 @@ import Mustache from 'mustache';
|
|
|
4
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
import { JSONPath } from 'jsonpath-plus';
|
|
7
|
-
import {
|
|
7
|
+
import { w as withErrorHandling } from '../shared/nuxt-llms-generator.bc139143.mjs';
|
|
8
8
|
import '@nuxt/kit';
|
|
9
9
|
import 'zod';
|
|
10
10
|
import 'node-html-markdown';
|
|
11
11
|
|
|
12
|
+
function buildLLMSTemplatePrompt(request) {
|
|
13
|
+
return `# LLMS.txt-Optimized Mustache Template Generator
|
|
14
|
+
|
|
15
|
+
You are an expert at creating **Mustache.js templates** that generate **LLM knowledge base entries** following the [\`llms.txt\` standard](https://llmstxt.org/).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## \u{1F3AF} TRUE PURPOSE: Help LLMs Answer Questions Efficiently
|
|
20
|
+
|
|
21
|
+
**Critical Understanding:**
|
|
22
|
+
These \`.md\` files are **NOT website copies** \u2014 they are **LLM knowledge base entries** designed for **inference** (understanding), not training.
|
|
23
|
+
|
|
24
|
+
**Primary Goal:** Enable LLMs to quickly answer user questions about this website page within **limited context windows** (typically 200K tokens).
|
|
25
|
+
|
|
26
|
+
**What This Means:**
|
|
27
|
+
- An LLM will read this file to understand "What is this page about?"
|
|
28
|
+
- Users will ask questions like "What does this page offer?", "Who is this for?", "What are the key features?"
|
|
29
|
+
- Content must be **concise**, **scannable**, and **fact-dense**
|
|
30
|
+
- Prioritize information that helps LLMs **infer meaning** over complete content reproduction
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## \u{1F4E5} Context Supplied
|
|
35
|
+
|
|
36
|
+
- **URL:** ${request.url}
|
|
37
|
+
- **Template Alias:** ${request.templateAlias}
|
|
38
|
+
- **JSON Path:** ${request.jpath}
|
|
39
|
+
|
|
40
|
+
### Available Data
|
|
41
|
+
\`\`\`json
|
|
42
|
+
${JSON.stringify(request.pageContent, null, 2)}
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## \u{1F9E0} Content Philosophy: Think "Knowledge Base Entry"
|
|
48
|
+
|
|
49
|
+
### 1. Start with Expert-Level Summary
|
|
50
|
+
- **First impression matters:** What would an expert say about this page in 1-2 sentences?
|
|
51
|
+
- Lead with **value proposition** or **core purpose**
|
|
52
|
+
- Use the blockquote format (\`> \`) for the summary \u2014 this signals importance
|
|
53
|
+
|
|
54
|
+
### 2. Structure for Question-Answering
|
|
55
|
+
Anticipate questions an LLM might need to answer:
|
|
56
|
+
- "What is this?" \u2192 Main heading + summary
|
|
57
|
+
- "What does it do/offer?" \u2192 Key features/benefits section
|
|
58
|
+
- "Who is it for?" \u2192 Target audience/use cases
|
|
59
|
+
- "How does it work?" \u2192 Process/methodology
|
|
60
|
+
- "What are the details?" \u2192 Technical specs/pricing/etc.
|
|
61
|
+
|
|
62
|
+
### 3. Prioritize Information by Importance
|
|
63
|
+
**Essential First:**
|
|
64
|
+
- What this page represents
|
|
65
|
+
- Primary value/purpose
|
|
66
|
+
- Key differentiators
|
|
67
|
+
|
|
68
|
+
**Supporting Details Second:**
|
|
69
|
+
- Features, benefits, specifications
|
|
70
|
+
- Use cases, examples
|
|
71
|
+
- Technical details
|
|
72
|
+
|
|
73
|
+
**Peripheral Information Last:**
|
|
74
|
+
- Meta information, related links
|
|
75
|
+
- Supplementary context
|
|
76
|
+
|
|
77
|
+
### 4. Optimize for Scanability
|
|
78
|
+
- Use **hierarchical headings** (\`#\`, \`##\`, \`###\`) to create clear structure
|
|
79
|
+
- Employ **bullet lists** for scannable facts
|
|
80
|
+
- Keep paragraphs **short and dense** (2-3 sentences max)
|
|
81
|
+
- Use **semantic Markdown** only \u2014 no HTML, entities, or attributes
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## \u{1F527} Technical Principles (Key-Agnostic Design)
|
|
86
|
+
|
|
87
|
+
### 1. Dynamic Property Inference
|
|
88
|
+
**Do not assume fixed property names.** Infer content type and importance from:
|
|
89
|
+
- **Value structure:** Object, array, string, number
|
|
90
|
+
- **Value length:** Short strings = titles; long text = descriptions
|
|
91
|
+
- **Position in JSON:** Root-level = high importance; nested = contextual details
|
|
92
|
+
- **Semantic patterns:** URLs, images, dates, IDs
|
|
93
|
+
|
|
94
|
+
### 2. Exact Property Bindings
|
|
95
|
+
- Always use the **exact property name** from JSON: \`{{actualKeyName}}\`
|
|
96
|
+
- Do NOT rename or modify binding identifiers
|
|
97
|
+
- The Mustache bindings must match JSON precisely
|
|
98
|
+
|
|
99
|
+
### 3. Humanized Section Headings
|
|
100
|
+
While bindings stay exact, convert keys to readable headings:
|
|
101
|
+
- \`productFeatures\` \u2192 "Product Features"
|
|
102
|
+
- \`pricing_tiers\` \u2192 "Pricing Tiers"
|
|
103
|
+
- \`techSpecs\` \u2192 "Technical Specifications"
|
|
104
|
+
|
|
105
|
+
### 4. Semantic Interpretation Guide
|
|
106
|
+
- **Short root strings (5-50 chars)** \u2192 Likely page title
|
|
107
|
+
- **Medium text (50-300 chars)** \u2192 Likely summary/tagline
|
|
108
|
+
- **Long text (300+ chars)** \u2192 Likely detailed description
|
|
109
|
+
- **Arrays of primitives** \u2192 Bullet lists
|
|
110
|
+
- **Arrays of objects** \u2192 Repeated sections or tables
|
|
111
|
+
- **Nested objects** \u2192 Sub-sections with logical hierarchy
|
|
112
|
+
- **URL-like strings** \u2192 Render as \`[Label]({{url}})\`
|
|
113
|
+
- **Image URLs** \u2192 Render as \`\`
|
|
114
|
+
|
|
115
|
+
### 5. Noise Filtering
|
|
116
|
+
**Exclude non-content fields:**
|
|
117
|
+
- IDs (\`id\`, \`nodeId\`, \`_id\`)
|
|
118
|
+
- Timestamps (\`createdAt\`, \`updatedAt\`, \`lastModified\`)
|
|
119
|
+
- Internal flags (\`isPublished\`, \`sortOrder\`, \`hidden\`)
|
|
120
|
+
- System metadata (\`_type\`, \`contentType\`, \`template\`)
|
|
121
|
+
|
|
122
|
+
### 6. Hierarchy & Nesting
|
|
123
|
+
- **Root level** \u2192 \`#\` (H1) \u2014 one per document
|
|
124
|
+
- **Primary sections** \u2192 \`##\` (H2)
|
|
125
|
+
- **Sub-sections** \u2192 \`###\` (H3)
|
|
126
|
+
- **Details** \u2192 \`####\` (H4) \u2014 avoid going deeper
|
|
127
|
+
- Heading depth corresponds to JSON nesting, but stay practical
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## \u{1F4D0} Template Structure Pattern
|
|
132
|
+
|
|
133
|
+
### Mandatory Opening
|
|
134
|
+
\`\`\`mustache
|
|
135
|
+
# {{primaryTitle}}
|
|
136
|
+
|
|
137
|
+
{{#summaryOrTagline}}
|
|
138
|
+
> {{summaryOrTagline}}
|
|
139
|
+
{{/summaryOrTagline}}
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
### Recommended Sections (adapt to JSON)
|
|
143
|
+
\`\`\`mustache
|
|
144
|
+
{{#mainDescription}}
|
|
145
|
+
{{mainDescription}}
|
|
146
|
+
{{/mainDescription}}
|
|
147
|
+
|
|
148
|
+
{{#keyFeatures.0}}
|
|
149
|
+
## Key Features
|
|
150
|
+
{{#keyFeatures}}
|
|
151
|
+
- **{{featureName}}**: {{featureDescription}}
|
|
152
|
+
{{/keyFeatures}}
|
|
153
|
+
{{/keyFeatures.0}}
|
|
154
|
+
|
|
155
|
+
{{#useCases.0}}
|
|
156
|
+
## Use Cases
|
|
157
|
+
{{#useCases}}
|
|
158
|
+
### {{caseTitle}}
|
|
159
|
+
{{caseDescription}}
|
|
160
|
+
{{/useCases}}
|
|
161
|
+
{{/useCases.0}}
|
|
162
|
+
|
|
163
|
+
{{#technicalDetails.0}}
|
|
164
|
+
## Technical Details
|
|
165
|
+
{{#technicalDetails}}
|
|
166
|
+
- **{{detailLabel}}**: {{detailValue}}
|
|
167
|
+
{{/technicalDetails}}
|
|
168
|
+
{{/technicalDetails.0}}
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
**Note:** This is an illustrative pattern. Adapt section names and structure to match the actual JSON dynamically.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## \u2705 Output Requirements
|
|
176
|
+
|
|
177
|
+
1. **Output ONLY the Mustache template** \u2014 no explanations, no code fences, no preamble
|
|
178
|
+
2. **Use exact JSON property names** in all bindings
|
|
179
|
+
3. **Generate clean Markdown** \u2014 no HTML, entities, or attributes
|
|
180
|
+
4. **Prioritize content** \u2014 most important information first
|
|
181
|
+
5. **Be concise** \u2014 optimize for limited context windows
|
|
182
|
+
6. **Structure for questions** \u2014 LLMs should easily extract facts
|
|
183
|
+
7. **Stay domain-agnostic** \u2014 template should work for any JSON shape
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## \u{1F680} Your Task
|
|
188
|
+
|
|
189
|
+
Analyze the provided JSON structure and **generate a Mustache template** that produces an **LLM knowledge base entry** following these principles.
|
|
190
|
+
|
|
191
|
+
**Think:**
|
|
192
|
+
- What would an LLM need to know to answer questions about this page?
|
|
193
|
+
- What's the core value/purpose this page communicates?
|
|
194
|
+
- How can I structure this for maximum inference efficiency?
|
|
195
|
+
|
|
196
|
+
Generate the template now.
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
12
200
|
class AnthropicClient {
|
|
13
201
|
client;
|
|
14
202
|
model;
|
|
@@ -49,121 +237,7 @@ class AnthropicClient {
|
|
|
49
237
|
throw new Error("Failed to generate template");
|
|
50
238
|
}
|
|
51
239
|
buildPrompt(request) {
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
**BUSINESS CONTEXT**: Voicenter provides enterprise cloud telephony solutions including Contact Centers, Business Phone Services, Mobile Communications, API integrations, and AI-powered voice tools for 50,000+ users globally.
|
|
55
|
-
|
|
56
|
-
**CURRENT PAGE ANALYSIS:**
|
|
57
|
-
- URL: ${request.url}
|
|
58
|
-
- Template: ${request.templateAlias}
|
|
59
|
-
- JSONPath: ${request.jpath}
|
|
60
|
-
|
|
61
|
-
**AVAILABLE DATA PROPERTIES:**
|
|
62
|
-
\`\`\`json
|
|
63
|
-
${JSON.stringify(request.pageContent, null, 2)}
|
|
64
|
-
\`\`\`
|
|
65
|
-
|
|
66
|
-
**CRITICAL REQUIREMENTS (2024 LLMS.txt Standard):**
|
|
67
|
-
|
|
68
|
-
1. **USE EXACT PROPERTY NAMES**:
|
|
69
|
-
\u274C Wrong: {{pageTitle}}
|
|
70
|
-
\u2705 Correct: {{pageTittle}} or {{pageDescription}} (match actual JSON keys)
|
|
71
|
-
|
|
72
|
-
2. **BUSINESS-FOCUSED CONTENT HIERARCHY**:
|
|
73
|
-
- H1: Clear service/feature name
|
|
74
|
-
- Blockquote: Value proposition
|
|
75
|
-
- H2: Key capabilities/benefits
|
|
76
|
-
- H3: Technical details/specs
|
|
77
|
-
- Lists: Features, integrations, use cases
|
|
78
|
-
|
|
79
|
-
3. **VOICENTER-SPECIFIC CONTENT MAPPING**:
|
|
80
|
-
- Service descriptions \u2192 Clear business benefits
|
|
81
|
-
- Technical features \u2192 User-friendly explanations
|
|
82
|
-
- Integration lists \u2192 Specific partner names
|
|
83
|
-
- API documentation \u2192 Implementation clarity
|
|
84
|
-
- Contact info \u2192 Business value context
|
|
85
|
-
|
|
86
|
-
4. **SMART CONTENT SELECTION**:
|
|
87
|
-
- Prioritize business value over technical jargon
|
|
88
|
-
- Include specific numbers/metrics when available
|
|
89
|
-
- Map complex nested arrays to structured lists
|
|
90
|
-
- Extract key differentiators and benefits
|
|
91
|
-
|
|
92
|
-
**TEMPLATE PATTERNS FOR VOICENTER CONTENT:**
|
|
93
|
-
|
|
94
|
-
**Service Pages Pattern:**
|
|
95
|
-
\`\`\`mustache
|
|
96
|
-
# {{serviceName}}
|
|
97
|
-
|
|
98
|
-
{{#serviceSubtitle}}
|
|
99
|
-
> {{serviceSubtitle}}
|
|
100
|
-
{{/serviceSubtitle}}
|
|
101
|
-
|
|
102
|
-
{{#serviceDescription}}
|
|
103
|
-
## Overview
|
|
104
|
-
{{serviceDescription}}
|
|
105
|
-
{{/serviceDescription}}
|
|
106
|
-
|
|
107
|
-
{{#serviceTools.0}}
|
|
108
|
-
## Key Features
|
|
109
|
-
{{#serviceTools}}
|
|
110
|
-
- {{textItem}}
|
|
111
|
-
{{/serviceTools}}
|
|
112
|
-
{{/serviceTools.0}}
|
|
113
|
-
|
|
114
|
-
{{#serviceLink}}
|
|
115
|
-
## Learn More
|
|
116
|
-
[Explore {{serviceName}} \u2192]({{serviceLink}})
|
|
117
|
-
{{/serviceLink}}
|
|
118
|
-
\`\`\`
|
|
119
|
-
|
|
120
|
-
**Feature/API Pages Pattern:**
|
|
121
|
-
\`\`\`mustache
|
|
122
|
-
# {{cardTitle}}
|
|
123
|
-
|
|
124
|
-
{{#cardText}}
|
|
125
|
-
> {{cardText}}
|
|
126
|
-
{{/cardText}}
|
|
127
|
-
|
|
128
|
-
{{#featureDescription}}
|
|
129
|
-
## How It Works
|
|
130
|
-
{{featureDescription}}
|
|
131
|
-
{{/featureDescription}}
|
|
132
|
-
|
|
133
|
-
{{#capabilities.0}}
|
|
134
|
-
## Capabilities
|
|
135
|
-
{{#capabilities}}
|
|
136
|
-
### {{name}}
|
|
137
|
-
{{description}}
|
|
138
|
-
|
|
139
|
-
{{/capabilities}}
|
|
140
|
-
{{/capabilities.0}}
|
|
141
|
-
|
|
142
|
-
## Business Benefits
|
|
143
|
-
- Reduces operational costs
|
|
144
|
-
- Improves customer experience
|
|
145
|
-
- Seamless integration with existing systems
|
|
146
|
-
\`\`\`
|
|
147
|
-
|
|
148
|
-
**CONTENT EXTRACTION RULES:**
|
|
149
|
-
1. **Identify primary content** from JSON structure
|
|
150
|
-
2. **Map nested arrays** to organized sections
|
|
151
|
-
3. **Extract business value** from technical descriptions
|
|
152
|
-
4. **Include contact/action items** for lead generation
|
|
153
|
-
5. **Maintain SEO-friendly** heading structure
|
|
154
|
-
|
|
155
|
-
**OUTPUT REQUIREMENTS:**
|
|
156
|
-
- Return ONLY the Mustache template
|
|
157
|
-
- NO explanations or code blocks
|
|
158
|
-
- Use actual property names from the provided JSON
|
|
159
|
-
- Focus on business value for AI consumption
|
|
160
|
-
- Follow LLMS.txt hierarchical structure
|
|
161
|
-
- **CLEAN MARKDOWN ONLY**: No HTML tags, entities, or attributes
|
|
162
|
-
- **NO HTML**: Use pure Markdown syntax (##, **, -, etc.)
|
|
163
|
-
- **NO ENTITIES**: Use actual characters, not & or /
|
|
164
|
-
- **NO ATTRIBUTES**: No dir="RTL", style="", class="" etc.
|
|
165
|
-
|
|
166
|
-
Generate the optimized Mustache template:`;
|
|
240
|
+
return buildLLMSTemplatePrompt(request);
|
|
167
241
|
}
|
|
168
242
|
parseResponse(responseText) {
|
|
169
243
|
const codeBlockRegex = /```(?:mustache)?\n?([\s\S]*?)```/;
|
|
@@ -884,245 +958,6 @@ async function performAutomaticCleanup(umbracoData, cacheDir, options = {}) {
|
|
|
884
958
|
return stats;
|
|
885
959
|
}
|
|
886
960
|
|
|
887
|
-
class MustacheSyntaxValidator {
|
|
888
|
-
name = "mustache-syntax";
|
|
889
|
-
canFix = true;
|
|
890
|
-
validate(template) {
|
|
891
|
-
const result = {
|
|
892
|
-
isValid: true,
|
|
893
|
-
errors: [],
|
|
894
|
-
warnings: []
|
|
895
|
-
};
|
|
896
|
-
try {
|
|
897
|
-
Mustache.parse(template);
|
|
898
|
-
return result;
|
|
899
|
-
} catch (error) {
|
|
900
|
-
result.isValid = false;
|
|
901
|
-
result.errors.push(`Mustache syntax error: ${error.message}`);
|
|
902
|
-
if (this.canFix) {
|
|
903
|
-
try {
|
|
904
|
-
const fixedTemplate = this.fix(template);
|
|
905
|
-
result.fixedTemplate = fixedTemplate;
|
|
906
|
-
result.warnings.push("Template was automatically fixed");
|
|
907
|
-
} catch (fixError) {
|
|
908
|
-
result.errors.push(`Could not fix template: ${fixError.message}`);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return result;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
fix(template) {
|
|
915
|
-
let fixedTemplate = template;
|
|
916
|
-
const openSectionRegex = /\{\{\#([a-zA-Z0-9_.]+)\}\}/g;
|
|
917
|
-
const closeSectionRegex = /\{\{\/([a-zA-Z0-9_.]+)\}\}/g;
|
|
918
|
-
const openSections = [];
|
|
919
|
-
const closeSections = [];
|
|
920
|
-
let match;
|
|
921
|
-
while ((match = openSectionRegex.exec(template)) !== null) {
|
|
922
|
-
openSections.push({
|
|
923
|
-
name: match[1],
|
|
924
|
-
pos: match.index
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
while ((match = closeSectionRegex.exec(template)) !== null) {
|
|
928
|
-
closeSections.push({
|
|
929
|
-
name: match[1],
|
|
930
|
-
pos: match.index
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
const unmatchedOpens = openSections.filter(
|
|
934
|
-
(open) => !closeSections.some((close) => close.name === open.name)
|
|
935
|
-
);
|
|
936
|
-
const unmatchedCloses = closeSections.filter(
|
|
937
|
-
(close) => !openSections.some((open) => open.name === close.name)
|
|
938
|
-
);
|
|
939
|
-
unmatchedOpens.forEach((unmatched) => {
|
|
940
|
-
const sectionRegex = new RegExp(`\\{\\{#${unmatched.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\}\\}`, "g");
|
|
941
|
-
fixedTemplate = fixedTemplate.replace(sectionRegex, "");
|
|
942
|
-
});
|
|
943
|
-
unmatchedCloses.forEach((unmatched) => {
|
|
944
|
-
const sectionRegex = new RegExp(`\\{\\{/${unmatched.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\}\\}`, "g");
|
|
945
|
-
fixedTemplate = fixedTemplate.replace(sectionRegex, "");
|
|
946
|
-
});
|
|
947
|
-
fixedTemplate = fixedTemplate.replace(/\n\s*\n\s*\n/g, "\n\n");
|
|
948
|
-
return fixedTemplate;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
class TemplateStructureValidator {
|
|
952
|
-
name = "template-structure";
|
|
953
|
-
validate(template) {
|
|
954
|
-
const result = {
|
|
955
|
-
isValid: true,
|
|
956
|
-
errors: [],
|
|
957
|
-
warnings: []
|
|
958
|
-
};
|
|
959
|
-
if (!template.match(/^#\s+/m)) {
|
|
960
|
-
result.warnings.push("Template should start with a heading (# Title)");
|
|
961
|
-
}
|
|
962
|
-
const headingLevels = (template.match(/^#{4,}/gm) || []).length;
|
|
963
|
-
if (headingLevels > 0) {
|
|
964
|
-
result.warnings.push("Template has deeply nested headings (4+ levels), consider flattening structure");
|
|
965
|
-
}
|
|
966
|
-
const emptySections = template.match(/\{\{#\w+\}\}\s*\{\{\/\w+\}\}/g);
|
|
967
|
-
if (emptySections) {
|
|
968
|
-
result.warnings.push(`Found ${emptySections.length} empty sections that may not render content`);
|
|
969
|
-
}
|
|
970
|
-
const commonTypos = template.match(/\{\{\s*pageTittle\s*\}\}/g);
|
|
971
|
-
if (commonTypos) {
|
|
972
|
-
result.warnings.push('Found "pageTittle" - check if this should be "pageTitle"');
|
|
973
|
-
}
|
|
974
|
-
return result;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
class ContentCompletenessValidator {
|
|
978
|
-
name = "content-completeness";
|
|
979
|
-
validate(template) {
|
|
980
|
-
const result = {
|
|
981
|
-
isValid: true,
|
|
982
|
-
errors: [],
|
|
983
|
-
warnings: []
|
|
984
|
-
};
|
|
985
|
-
const variables = this.extractVariables(template);
|
|
986
|
-
const hasTitle = variables.some((v) => v.includes("title") || v.includes("Title"));
|
|
987
|
-
if (!hasTitle) {
|
|
988
|
-
result.warnings.push("Template missing title variable (pageTitle, title, etc.)");
|
|
989
|
-
}
|
|
990
|
-
const hasDescription = variables.some((v) => v.includes("description") || v.includes("Description"));
|
|
991
|
-
if (!hasDescription) {
|
|
992
|
-
result.warnings.push("Template missing description variable");
|
|
993
|
-
}
|
|
994
|
-
if (template.length < 50) {
|
|
995
|
-
result.warnings.push("Template is very short, may not provide sufficient content");
|
|
996
|
-
}
|
|
997
|
-
const sectionsOnly = variables.filter((v) => !v.includes(".") && !v.includes("["));
|
|
998
|
-
if (sectionsOnly.length < 2) {
|
|
999
|
-
result.warnings.push("Template has limited content variables, consider adding more sections");
|
|
1000
|
-
}
|
|
1001
|
-
return result;
|
|
1002
|
-
}
|
|
1003
|
-
extractVariables(template) {
|
|
1004
|
-
const variableRegex = /\{\{\s*([^#\/][^}]*?)\s*\}\}/g;
|
|
1005
|
-
const variables = [];
|
|
1006
|
-
let match;
|
|
1007
|
-
while ((match = variableRegex.exec(template)) !== null) {
|
|
1008
|
-
variables.push(match[1].trim());
|
|
1009
|
-
}
|
|
1010
|
-
return variables;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
class LLMSTxtComplianceValidator {
|
|
1014
|
-
name = "llms-txt-compliance";
|
|
1015
|
-
validate(template) {
|
|
1016
|
-
const result = {
|
|
1017
|
-
isValid: true,
|
|
1018
|
-
errors: [],
|
|
1019
|
-
warnings: []
|
|
1020
|
-
};
|
|
1021
|
-
const headings = template.match(/^#+\s+.+$/gm) || [];
|
|
1022
|
-
let lastLevel = 0;
|
|
1023
|
-
let hasProperHierarchy = true;
|
|
1024
|
-
headings.forEach((heading) => {
|
|
1025
|
-
const level = (heading.match(/^#+/) || [""])[0].length;
|
|
1026
|
-
if (level > lastLevel + 1) {
|
|
1027
|
-
hasProperHierarchy = false;
|
|
1028
|
-
}
|
|
1029
|
-
lastLevel = level;
|
|
1030
|
-
});
|
|
1031
|
-
if (!hasProperHierarchy) {
|
|
1032
|
-
result.warnings.push("Heading hierarchy should increment by one level (# -> ## -> ###)");
|
|
1033
|
-
}
|
|
1034
|
-
if (template.includes("pageDescription") && !template.includes(">")) {
|
|
1035
|
-
result.warnings.push("Consider using blockquote (>) for page description as per LLMS.txt standard");
|
|
1036
|
-
}
|
|
1037
|
-
const hasLists = template.includes("- ") || template.includes("* ");
|
|
1038
|
-
if (!hasLists && template.length > 200) {
|
|
1039
|
-
result.warnings.push("Long content without lists - consider breaking into bullet points for better AI consumption");
|
|
1040
|
-
}
|
|
1041
|
-
const htmlTags = (template.match(/<[^>]+>/g) || []).length;
|
|
1042
|
-
if (htmlTags > 3) {
|
|
1043
|
-
result.warnings.push("Template contains HTML tags - prefer pure markdown for LLMS.txt compliance");
|
|
1044
|
-
}
|
|
1045
|
-
return result;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
class TemplateValidationPipeline {
|
|
1049
|
-
validators = [];
|
|
1050
|
-
constructor() {
|
|
1051
|
-
this.addValidator(new MustacheSyntaxValidator());
|
|
1052
|
-
this.addValidator(new TemplateStructureValidator());
|
|
1053
|
-
this.addValidator(new ContentCompletenessValidator());
|
|
1054
|
-
this.addValidator(new LLMSTxtComplianceValidator());
|
|
1055
|
-
}
|
|
1056
|
-
addValidator(validator) {
|
|
1057
|
-
this.validators.push(validator);
|
|
1058
|
-
}
|
|
1059
|
-
removeValidator(name) {
|
|
1060
|
-
this.validators = this.validators.filter((v) => v.name !== name);
|
|
1061
|
-
}
|
|
1062
|
-
async validateTemplate(template, options = {}) {
|
|
1063
|
-
const { autoFix = true, throwOnError = false } = options;
|
|
1064
|
-
let currentTemplate = template;
|
|
1065
|
-
const allResults = {
|
|
1066
|
-
isValid: true,
|
|
1067
|
-
errors: [],
|
|
1068
|
-
warnings: []
|
|
1069
|
-
};
|
|
1070
|
-
for (const validator of this.validators) {
|
|
1071
|
-
const result = validator.validate(currentTemplate);
|
|
1072
|
-
allResults.errors.push(...result.errors);
|
|
1073
|
-
allResults.warnings.push(...result.warnings);
|
|
1074
|
-
if (!result.isValid) {
|
|
1075
|
-
allResults.isValid = false;
|
|
1076
|
-
if (autoFix && validator.canFix && validator.fix) {
|
|
1077
|
-
const fixedTemplate = validator.fix(currentTemplate);
|
|
1078
|
-
const fixResult = validator.validate(fixedTemplate);
|
|
1079
|
-
if (fixResult.isValid) {
|
|
1080
|
-
currentTemplate = fixedTemplate;
|
|
1081
|
-
allResults.fixedTemplate = currentTemplate;
|
|
1082
|
-
console.log(`Template fixed by ${validator.name} validator`);
|
|
1083
|
-
allResults.errors = allResults.errors.filter((e) => !result.errors.includes(e));
|
|
1084
|
-
if (allResults.errors.length === 0) {
|
|
1085
|
-
allResults.isValid = true;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (allResults.errors.length > 0 && throwOnError) {
|
|
1092
|
-
throw new TemplateError(
|
|
1093
|
-
ErrorCode.TEMPLATE_VALIDATION_FAILED,
|
|
1094
|
-
`Template validation failed: ${allResults.errors.join(", ")}`,
|
|
1095
|
-
{ template: template.substring(0, 200) + "..." }
|
|
1096
|
-
);
|
|
1097
|
-
}
|
|
1098
|
-
return allResults;
|
|
1099
|
-
}
|
|
1100
|
-
async validateAndFix(template) {
|
|
1101
|
-
const result = await this.validateTemplate(template, {
|
|
1102
|
-
autoFix: true,
|
|
1103
|
-
throwOnError: false
|
|
1104
|
-
});
|
|
1105
|
-
if (result.fixedTemplate) {
|
|
1106
|
-
return result.fixedTemplate;
|
|
1107
|
-
}
|
|
1108
|
-
if (result.errors.length > 0) {
|
|
1109
|
-
console.warn("Could not fix template, using fallback");
|
|
1110
|
-
return `# {{pageTitle}}
|
|
1111
|
-
|
|
1112
|
-
> {{pageDescription}}
|
|
1113
|
-
|
|
1114
|
-
## Content
|
|
1115
|
-
|
|
1116
|
-
This page content could not be processed due to template formatting issues.`;
|
|
1117
|
-
}
|
|
1118
|
-
return template;
|
|
1119
|
-
}
|
|
1120
|
-
getValidatorNames() {
|
|
1121
|
-
return this.validators.map((v) => v.name);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
const templateValidationPipeline = new TemplateValidationPipeline();
|
|
1125
|
-
|
|
1126
961
|
class TemplateGenerator {
|
|
1127
962
|
anthropicClient;
|
|
1128
963
|
promptAnalyzer;
|
|
@@ -1267,8 +1102,7 @@ class TemplateGenerator {
|
|
|
1267
1102
|
}
|
|
1268
1103
|
async renderTemplate(template, data) {
|
|
1269
1104
|
return withErrorHandling(async () => {
|
|
1270
|
-
|
|
1271
|
-
return Mustache.render(validatedTemplate, data);
|
|
1105
|
+
return Mustache.render(template, data);
|
|
1272
1106
|
}, {
|
|
1273
1107
|
template: template.substring(0, 200) + "...",
|
|
1274
1108
|
dataKeys: Object.keys(data)
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -58,28 +58,6 @@ class SchemaValidator {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
62
|
-
ErrorCode2["INVALID_CONFIG"] = "INVALID_CONFIG";
|
|
63
|
-
ErrorCode2["MISSING_API_KEY"] = "MISSING_API_KEY";
|
|
64
|
-
ErrorCode2["INVALID_DATA_PATH"] = "INVALID_DATA_PATH";
|
|
65
|
-
ErrorCode2["INVALID_UMBRACO_DATA"] = "INVALID_UMBRACO_DATA";
|
|
66
|
-
ErrorCode2["PAGE_CONTENT_EXTRACTION_FAILED"] = "PAGE_CONTENT_EXTRACTION_FAILED";
|
|
67
|
-
ErrorCode2["INVALID_JPATH"] = "INVALID_JPATH";
|
|
68
|
-
ErrorCode2["ANTHROPIC_API_ERROR"] = "ANTHROPIC_API_ERROR";
|
|
69
|
-
ErrorCode2["ANTHROPIC_CONNECTION_FAILED"] = "ANTHROPIC_CONNECTION_FAILED";
|
|
70
|
-
ErrorCode2["TEMPLATE_GENERATION_FAILED"] = "TEMPLATE_GENERATION_FAILED";
|
|
71
|
-
ErrorCode2["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
|
|
72
|
-
ErrorCode2["TEMPLATE_VALIDATION_FAILED"] = "TEMPLATE_VALIDATION_FAILED";
|
|
73
|
-
ErrorCode2["TEMPLATE_RENDERING_FAILED"] = "TEMPLATE_RENDERING_FAILED";
|
|
74
|
-
ErrorCode2["MUSTACHE_SYNTAX_ERROR"] = "MUSTACHE_SYNTAX_ERROR";
|
|
75
|
-
ErrorCode2["FILE_READ_ERROR"] = "FILE_READ_ERROR";
|
|
76
|
-
ErrorCode2["FILE_WRITE_ERROR"] = "FILE_WRITE_ERROR";
|
|
77
|
-
ErrorCode2["DIRECTORY_CREATION_FAILED"] = "DIRECTORY_CREATION_FAILED";
|
|
78
|
-
ErrorCode2["CACHE_READ_ERROR"] = "CACHE_READ_ERROR";
|
|
79
|
-
ErrorCode2["CACHE_WRITE_ERROR"] = "CACHE_WRITE_ERROR";
|
|
80
|
-
ErrorCode2["CACHE_CORRUPTED"] = "CACHE_CORRUPTED";
|
|
81
|
-
return ErrorCode2;
|
|
82
|
-
})(ErrorCode || {});
|
|
83
61
|
class LLMSError extends Error {
|
|
84
62
|
code;
|
|
85
63
|
context;
|
|
@@ -117,12 +95,6 @@ class AnthropicAPIError extends LLMSError {
|
|
|
117
95
|
this.retryable = retryable;
|
|
118
96
|
}
|
|
119
97
|
}
|
|
120
|
-
class TemplateError extends LLMSError {
|
|
121
|
-
constructor(code, message, context, cause) {
|
|
122
|
-
super(code, message, context, cause);
|
|
123
|
-
this.name = "TemplateError";
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
98
|
class FileSystemError extends LLMSError {
|
|
127
99
|
path;
|
|
128
100
|
constructor(code, message, path, context, cause) {
|
|
@@ -379,4 +351,4 @@ async function generateLLMSFiles(config, umbracoData, logger) {
|
|
|
379
351
|
}
|
|
380
352
|
}
|
|
381
353
|
|
|
382
|
-
export {
|
|
354
|
+
export { llmsModule as l, withErrorHandling as w };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voicenter-team/nuxt-llms-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Nuxt 3 module for automatically generating AI-optimized documentation files (llms.txt, llms-full.txt, and individual .md files) from Umbraco CMS data using Anthropic's Claude API.",
|
|
5
5
|
"repository": "https://github.com/VoicenterTeam/nuxt-llms-generator",
|
|
6
6
|
"license": "MIT",
|