This commit is contained in:
lovebird 2025-04-01 18:33:46 +02:00
parent 55dae7814d
commit 1833ce2c2e
13 changed files with 78 additions and 217 deletions

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-04-01T07:22:34.370Z"
"default": "2025-04-01T16:27:18.441Z"
},
"description": {
"type": "string",

File diff suppressed because one or more lines are too long

View File

@ -670,7 +670,7 @@
},
"https://scholarworks.uni.edu/cgi/viewcontent.cgi?article=3680%5C&context=grp": {
"isValid": false,
"timestamp": 1743492156147
"timestamp": 1743522204145
},
"https://pmc.ncbi.nlm.nih.gov/articles/PMC10489002/": {
"isValid": true,
@ -754,7 +754,7 @@
},
"https://journals.plos.org/plosone/article?id=10.1371%252Fjournal.pone.0288696": {
"isValid": false,
"timestamp": 1743492156605
"timestamp": 1743522204880
},
"https://www.youtube.com/watch?v=_a7usMe_K38": {
"isValid": true,
@ -841,7 +841,7 @@
},
"https://www.toraytac.com/media/c3feb206-1398-4e0e-bca6-df7780f11745/tcCurg/TenCate%2520Advanced%2520Composites/Documents/Technical%2520papers/TenCate_chopped_fiber_thermoplastics_compression_molding_technical_paper.pdf": {
"isValid": false,
"timestamp": 1743492156759
"timestamp": 1743522205416
},
"https://youtu.be/qtZv96cifIU": {
"isValid": true,
@ -10509,5 +10509,15 @@
"image": "https://cdn.prod.website-files.com/67055e139de63bfa7ce3b278/670997a273d96fadb9f33c39_INV-HERO-Table%202x.jpg",
"favicon": "https://cdn.prod.website-files.com/67055e139de63bfa7ce3b278/67abc6213b55379f6dd37f60_favicon.ico"
}
},
"https://thenounproject.com/": {
"isValid": true,
"timestamp": 1743523186345,
"meta": {
"title": "Noun Project: Free Icons & Stock Photos for Everything",
"description": "Noun Project has the most diverse collection of free icons and stock photos. Download SVG and PNG. Over 7 million art-quality icons and free photos.",
"image": "https://thenounproject.com/_next/static/media/row1-1-freshly-washed-batch-of-cherries-photo.3b89e0de.jpg",
"favicon": "https://thenounproject.com/favicon-32x32.png"
}
}
}

View File

@ -3,7 +3,7 @@
"messages": [
{
"role": "user",
"content": "Create a concise description for SEO meta (around 150-160 characters) from the text below. \n Disregard any links or image references. \n Return only the final meta description, no extra commentary.\n\nText to process:\nThis tutorial outlines the process of cutting HDPE sheets with an X-Carve CNC machine.\n\nWatch the full video in Spanish with subtitles: [YouTube](https://www.youtube.com/watch?v=4LrrFz802To)\n\n\nUser Location: Mexico City, Mexico\n\n### Measurement and Setup\n\nMeasure the plastic sheet's height, width, and thickness. The X-Carve CNC machine uses the Easel CAM software, which allows simulation of materials, including HDPE 2-Colors. \n\nFor reference: \n- Height: ___ meters (___ inches) \n- Width: ___ meters (___ inches) \n- Thickness: ___ millimeters (___ inches)\n\nSecure the sheet to the table using the CNC clamps from the X-Carve.\n\nProceed to a vector graphics editor like Inkscape to create or download a vector file from sites such as [The Noun Project](https://thenounproject.com).\n\nDownload the SVG file, an open-source vector format, and import it into Easel.\n\nWith the file ready, select the desired carving width and initiate the cutting process:\n\n- Ensure the sheet is secured.\n- Specify the cutting bit, such as a 1/8 inch (3.175 mm) flat flute bit.\n- Set the machines coordinate origin at the lower-left corner.\n- Raise the bit and activate the CNC Router.\n\n**CNC Processing of Various Materials**\n\nTypically, we cut wood, acrylic, and aluminum using the CNC. Transitioning to plastic, specifically HDPE, proved uncomplicated. The CNC Router handles HDPE with ease, outpacing wood and vastly surpassing aluminum in speed.\n\nOur primary challenge with HDPE sheets arises from uneven surfaces due to our production process, leading to inconsistent cuts and engravings. To address this, we often perform an initial CNC pass to level the sheet's surface.\n\n### Final Version\n\nTake your glasses or object, post-process them, and share the results with others."
"content": "Return a list of max. 10 keywords that can be used for SEO purposes, separated by commas (dont comment, just the list) : \n\nText to process:\nThis guide outlines the process for cutting HDPE sheets using an X-Carve CNC machine.\n\nFor a detailed demonstration, watch the video with subtitles: [Watch Video](https://www.youtube.com/watch?v=4LrrFz802To)\n\n\nUser Location: Mexico City, Mexico\n\nFor this step, measure the plastic sheet's height, width, and thickness. The X-Carve machine uses the CAM software Easel, which is very user-friendly for CNC milling. Easel allows you to simulate your material and includes HDPE 2-Colors in its material options.\n\nUsing clamps, secure the sheet to the table.\n\nOpen a vector design program like Inkscape to create or download a vector file from a suitable online resource.\n\nDownload the SVG file and import it into Easel.\n\nWith the file ready, select the desired width for carving or cutting. Follow these steps to begin:\n\n- Ensure the sheet is secured.\n- Specify the cutting tool, using a 1/8 inch (3.175 mm) flat flute bit.\n- Set the machine's origin point to the bottom left corner.\n- Raise the bit and activate the CNC Router.\n\nAnd then the process starts!\n\nTake your glasses or object, post-process them, and show the results to friends and family.\n\nYou can attempt this project with various CNC machines, including manual routers or saws, as demonstrated in this video: [YouTube Link](https://youtu.be/gxkcffQD3eQ). Sharing your work supports community growth.\n\nPlease share your ideas and comments."
},
{
"role": "user",

View File

@ -2,6 +2,7 @@ import { IKBotTask } from "@polymech/kbot-d";
import { sync as read } from "@polymech/fs/read";
import { sync as exists } from "@polymech/fs/exists";
import { z } from "zod";
import { logger } from "./index.js";
// Type definitions
const InstructionSchema = z.object({
@ -18,7 +19,7 @@ export interface TemplateProps extends IKBotTask {
disabled?: boolean;
template?: string;
renderer?: string;
}
const TemplateConfigSchema = z.object({
@ -28,14 +29,13 @@ const TemplateConfigSchema = z.object({
preferences: z.string(),
mode: z.string(),
filters: z.string().optional(),
overrides: z.record(z.array(z.string())).optional(),
variables: z.record(z.string()).optional()
});
type TemplateConfig = z.infer<typeof TemplateConfigSchema>;
const LLMConfigSchema = z.object({
templates: z.record(TemplateConfigSchema),
options: z.record(TemplateConfigSchema),
instructions: InstructionSetSchema.optional(),
defaults: z.record(z.array(z.string())).optional()
});
@ -50,120 +50,64 @@ export const enum TemplateContext {
}
// Default configuration
export const DEFAULT_CONFIG: LLMConfig = {
templates: {},
options: {},
instructions: {},
defaults: {
tone: ["formal"],
content: ["spellCheck", "removeEmojis", "removePersonalPrefs", "shorten"],
moderation: ["mafiaFilter", "deprogramming"],
context: ["makerTutorials", "units"],
format: ["markdown"]
}
defaults: {}
};
// Default template options
const DEFAULT_TEMPLATE_OPTIONS = {
mode: "completion",
preferences: "none",
filters: "code"
} as const;
const getConfigPath = (context: TemplateContext): string => {
return `./src/config/templates/${context}.json`;
};
// Load configuration from JSON file
export const load = (context: TemplateContext = TemplateContext.COMMONS): LLMConfig => {
const configPath = `./src/config/templates/${context}.json`;
const configPath = getConfigPath(context);
if (exists(configPath)) {
try {
const content = read(configPath);
if (typeof content === 'string') {
const parsed = JSON.parse(content);
return LLMConfigSchema.parse(parsed);
}
const content = read(configPath, 'json') || {};
return LLMConfigSchema.parse(content)
} catch (error) {
console.error(`Error loading ${context} config:`, error);
logger.error(`Error loading ${context} config:`, error);
}
} else {
logger.error(`Config file ${configPath} not found`);
}
return DEFAULT_CONFIG;
};
// Helper function to build prompt from instruction sets
export const buildPrompt = (
instructions: z.infer<typeof InstructionSetSchema>,
defaults: Record<string, string[]>,
overrides?: Record<string, string[]>
defaults: Record<string, string[]>
): string => {
const getInstructions = (category: string, flags: string[]) => {
const set = instructions[category] || [];
return set.filter(x => flags.includes(x.flag)).map(x => x.text);
};
const merged = Object.keys(instructions).reduce((acc, category) => ({
...acc,
[category]: (overrides?.[category] ?? defaults[category] ?? [])
[category]: defaults[category] ?? []
}), {} as Record<string, string[]>);
return Object.entries(merged)
.flatMap(([category, flags]) => getInstructions(category, flags))
.join("\n");
};
// Prompt system
const PromptSchema = z.object({
template: z.string(),
variables: z.record(z.string()).optional(),
format: z.enum(['text', 'json', 'markdown']).default('text')
format: z.enum(['text', 'json', 'markdown', 'schema']).default('text')
});
type Prompt = z.infer<typeof PromptSchema>;
const PromptRegistrySchema = z.record(PromptSchema);
type PromptRegistry = z.infer<typeof PromptRegistrySchema>;
const DEFAULT_PROMPTS: PromptRegistry = {
seo: {
template: "Analyze the following content and return a JSON object with these fields: keywords (array of max 10 strings), title (string), description (string), tags (array of max 5 strings). Format as valid JSON only.",
format: 'json'
},
keywords: {
template: "Return a list of max. 10 keywords that can be used for SEO purposes, separated by commas (dont comment, just the list) : ",
format: 'text'
},
references: {
template: "Return a list of references (only with links), as Markdown, grouped : Articles, Books, Papers, Youtube, Opensource Designs, ... Dont comment !",
format: 'markdown'
},
brief: {
template: "Create a concise description for SEO meta (around 150-160 characters) from the text below. \n Disregard any links or image references. \n Return only the final meta description, no extra commentary.",
format: 'text'
},
toolsAndHardware: {
template: "Extract the required tools, software hardware from the following tutorial. Return as Markdown chapters (H3) with very short bullet points (not bold), with links, max. 5. per category.",
format: 'markdown'
},
learnedSkills: {
template: "Analyze the following tutorial and identify all the skills that a person would learn or improve by completing this project, modest, humble, simple - dont enflate (sustainable, recylcing, ...), Return as Markdown chapter (H2) with very short bullet points (not bold), max. 5.",
format: 'markdown'
},
local: {
template: "Markdown chapter (h4) with a list of local resources, services & suppliers, max. 5, with links, group by category. dont comment, just the list",
format: 'markdown'
}
}
const renderPrompt = (prompt: Prompt, variables: Record<string, string> = {}): string => {
let result = prompt.template;
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
return result;
}
const createTemplate = (config: LLMConfig, name: string, defaults: Partial<TemplateConfig>, promptKey?: keyof PromptRegistry) => {
const createTemplate = (config: LLMConfig, name: string, defaults: Partial<TemplateConfig>) => {
return (opts: Partial<TemplateConfig> = {}) => {
const template = config.templates[name] || defaults;
const prompt = promptKey ? renderPrompt(DEFAULT_PROMPTS[promptKey], opts.variables) : buildPrompt(
const template = config.options[name] || defaults;
const prompt = buildPrompt(
config.instructions || {},
config.defaults || {},
opts.overrides || template.overrides
)
config.defaults || {}
);
const merged = {
...template,
...opts,
@ -175,86 +119,17 @@ const createTemplate = (config: LLMConfig, name: string, defaults: Partial<Templ
export const createTemplates = (context: TemplateContext = TemplateContext.COMMONS) => {
const config = load(context);
switch (context) {
case TemplateContext.COMMONS:
return {
seo: createTemplate(config, 'seo', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}, 'seo'),
simple: createTemplate(config, 'simple', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}),
codeSimple: createTemplate(config, 'codeSimple', {
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}),
research: createTemplate(config, 'research', {
router: "openai",
model: "gpt-4.5-preview",
...DEFAULT_TEMPLATE_OPTIONS
})
};
case TemplateContext.HOWTO:
return {
keywords: createTemplate(config, 'keywords', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}, 'keywords'),
brief: createTemplate(config, 'keywords', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}, 'brief'),
references: createTemplate(config, 'references', {
model: "perplexity/sonar-deep-research",
...DEFAULT_TEMPLATE_OPTIONS
}, 'references'),
toolsAndHardware: createTemplate(config, 'toolsAndHardware', {
model: "perplexity/sonar-deep-research",
...DEFAULT_TEMPLATE_OPTIONS
}, 'toolsAndHardware'),
requiredSkills: createTemplate(config, 'requiredSkills', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}),
learnedSkills: createTemplate(config, 'learnedSkills', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
}, 'learnedSkills'),
local: createTemplate(config, 'local', {
model: "perplexity/sonar-deep-research",
...DEFAULT_TEMPLATE_OPTIONS
}, 'local')
};
case TemplateContext.MARKETPLACE:
return {
productDescription: createTemplate(config, 'productDescription', {
router: "openai",
model: "gpt-4o",
...DEFAULT_TEMPLATE_OPTIONS
})
};
default:
return {};
}
return Object.keys(config.options).reduce((acc, name) => ({
...acc,
[name]: createTemplate(config, name, {})
}), {});
};
// Export default templates with default config (commons context)
export const templates = createTemplates(TemplateContext.COMMONS);
// Export types
export type { TemplateConfig, LLMConfig, Prompt, PromptRegistry }
export { InstructionSchema,
InstructionSetSchema,
TemplateConfigSchema,
LLMConfigSchema,
PromptSchema,
PromptRegistrySchema };
export {
InstructionSchema,
InstructionSetSchema,
TemplateConfigSchema,
LLMConfigSchema,
PromptSchema,
PromptRegistrySchema
};

View File

@ -2,25 +2,20 @@ import { get_cached_object, set_cached_object } from "@polymech/cache"
import { run, OptionsSchema } from "@polymech/kbot-d";
import { resolveVariables } from "@polymech/commons/variables"
import { } from "@polymech/core/objects"
import { logger } from "./index.js"
const CACHE = true
import { logger, env } from "./index.js"
import { removeEmptyObjects } from "@/base/objects.js"
import { LLM_CACHE } from "@/config/config.js"
import {
TemplateProps,
TemplateContext,
createTemplates
} from "./kbot-templates.js";
import { env } from "./index.js";
export interface Props extends TemplateProps {
context?: TemplateContext;
}
const removeEmpty = (obj: any) => {
return Object.fromEntries(
Object.entries(obj).filter(([_, v]) => v != null && v !== '')
);
};
export const filter = async (content: string, tpl: string = 'howto', opts: Props = {}) => {
if (!content || content.length < 20) {
return content;
@ -40,20 +35,20 @@ export const filter = async (content: string, tpl: string = 'howto', opts: Props
filters: [],
tools: []
};
const ca_options = JSON.parse(JSON.stringify(removeEmpty(cache_key_obj)));
const ca_options = JSON.parse(JSON.stringify(removeEmptyObjects(cache_key_obj)));
let cached = null
try {
cached = await get_cached_object({ ca_options }, 'kbot');
} catch (e) {
logger.error(`Failed to get cached object for ${content.substring(0, 20)}`, e);
}
if (cached && CACHE) {
if (cached && LLM_CACHE) {
return cached;
}
logger.info(`kbot: template:${tpl} : context:${context} @ ${options.model} : ${content.substring(0, 20)}`)
const result = await run(options);
if(!result || !result[0]){
logger.error(`No result for ${content.substring(0, 20)}`)
if (!result || !result[0]) {
logger.error(`No result for ${content.substring(0, 20)}`)
return content;
}
if (template.format === 'json') {
@ -70,9 +65,8 @@ export const filter = async (content: string, tpl: string = 'howto', opts: Props
logger.debug(`caching result for "${content.substring(0, 20)}..."`)
return result[0] as string;
};
export const template_filter = async (text: string, template: string, context: TemplateContext = TemplateContext.COMMONS) => {
if(!text || text.length < 20) {
if (!text || text.length < 20) {
return text;
}
const templates = createTemplates(context);
@ -96,11 +90,10 @@ export const template_filter = async (text: string, template: string, context: T
});
return ret;
};
export const getFilterOptions = (content: string, template: any, opts: Props = {}) => {
return OptionsSchema().parse({
...template,
prompt: `${template.prompt || ""} : ${content}`,
...opts,
});
};
};

View File

@ -121,7 +121,7 @@ const EditLink = () => {
<div class="flex flex-wrap gap-2 mb-4">
{
howto.tags.map((tag) => (
<span class="bg-orange-500 text-white text-xs px-3 py-1 rounded-full">
<span class="bg-orange-400 text-white text-xs px-3 py-1 rounded-full">
<Translate>{tag.toUpperCase()}</Translate>
</span>
))

View File

@ -4,6 +4,10 @@ import { resolve, template } from '@polymech/commons'
import { sync as read } from '@polymech/fs/read'
import { sanitizeUri } from 'micromark-util-sanitize-uri'
// LLM
export const LLM_CACHE = false
export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}"))
export const LOGGING_NAMESPACE = 'polymech-site'
@ -33,7 +37,7 @@ export const HOWTO_ADD_REFERENCES = true
export const HOWTO_COMPLETE_SKILLS = false
export const HOWTO_LOCAL_RESOURCES = false
export const HOWTO_SEO_LLM = true
export const HOWTO_MAX_ITEMS = 10
export const HOWTO_MAX_ITEMS = 1
export const HOWTO_MIGRATION = () => path.resolve(resolve("./data/last.json"))
export const HOWTO_ROOT_INTERN = () => path.resolve(resolve("./public/resources/howtos"))
@ -56,7 +60,7 @@ export const DIRECTORY_ADD_REFERENCES = true
export const DIRECTORY_COMPLETE_SKILLS = false
export const DIRECTORY_LOCAL_RESOURCES = false
export const DIRECTORY_SEO_LLM = true
export const DIRECTORY_MAX_ITEMS = 20
export const DIRECTORY_MAX_ITEMS = 0
export const DIRECTORY_MIGRATION = () => path.resolve(resolve("./data/last.json"))
export const DIRECTORY_ROOT_INTERN = () => path.resolve(resolve("./public/resources/directory"))

View File

@ -1,5 +1,5 @@
{
"templates": {
"options": {
"simple": {
"router": "openai",
"model": "gpt-4o",
@ -7,11 +7,6 @@
"mode": "completion",
"filters": "code"
},
"codeSimple": {
"model": "gpt-4o",
"preferences": "none",
"mode": "completion"
},
"research": {
"model": "perplexity/sonar-deep-research",
"preferences": "none",
@ -21,7 +16,7 @@
"instructions": {
"tone": [
{ "flag": "formal", "text": "use a formal tone" },
{ "flag": "friendly", "text": "be friendly and approachable" }
{ "flag": "friendly", "text": "Be friendly and approachable!" }
],
"content": [
{ "flag": "spellCheck", "text": "spell & grammar fix the text," },

View File

@ -1,5 +1,5 @@
{
"templates": {
"options": {
"keywords": {
"router": "openai",
"model": "gpt-4o",
@ -25,7 +25,7 @@
"model": "perplexity/sonar-deep-research",
"preferences": "none",
"mode": "completion",
"prompt": "Extract the required tools, software hardware from the following tutorial.Return as Markdown chapters (H3) with very short bullet points (not bold), max. 5 per category.",
"prompt": "Extract the required tools: software, hardware from the following tutorial.Return as Markdown chapters (H3) with very short bullet points (not bold), max. 5 per category.",
"filters": "code"
},
"requiredSkills": {
@ -59,13 +59,10 @@
{ "flag": "safety", "text": "Include safety considerations and warnings" },
{ "flag": "troubleshooting", "text": "Include common issues and solutions" }
],
"format": [
{ "flag": "stepByStep", "text": "Format as clear step-by-step instructions" },
{ "flag": "checklist", "text": "Include progress checkpoints" }
]
"format": []
},
"defaults": {
"context": ["makerTutorials", "units", "safety", "troubleshooting"],
"format": ["stepByStep", "checklist"]
"format": []
}
}

View File

@ -3,14 +3,11 @@ import { findUp } from 'find-up'
import pMap from 'p-map'
import { sanitizeFilename } from "@polymech/fs/utils"
import { execFileSync, execFile } from "child_process";
import { execFile } from "child_process";
import { sync as read } from '@polymech/fs/read'
import { sync as exists } from '@polymech/fs/exists'
import { sync as mkdir } from '@polymech/fs/dir'
import { sync as rm } from '@polymech/fs/remove'
import { sync as write } from '@polymech/fs/write'
import type { Loader, LoaderContext } from 'astro/loaders'
import { resolveVariables } from "@polymech/commons/variables"
export * from './howto-model.js'
export * from '@/base/filters.js'
@ -27,7 +24,6 @@ import {
HOWTO_FILES_WEB,
HOWTO_FILES_ABS,
HOWTO_FILTER_LLM,
default_image,
HOWTO_ROOT,
HOWTO_GLOB,
@ -43,11 +39,7 @@ import {
HOWTO_MAX_ITEMS
} from "config/config.js"
import { env, logger } from '@/base/index.js'
import { logger } from '@/base/index.js'
import { applyFilters, default_filters_plain, FilterFunction } from '../../base/filters.js'
import { TemplateContext, buildPrompt, LLMConfig, createTemplates } from '@/base/kbot-templates.js';
import { template_filter } from '@/base/kbot.js'
@ -57,7 +49,6 @@ import { template_filter } from '@/base/kbot.js'
// Assets
//
/////////////////////////////////////////////////////////////////////////
export const item_path = (item: IHowto) => `${HOWTO_ROOT()}/${item.slug}`
export const asset_local_abs = async (item: IHowto, asset: IImage) => {
const sanitizedFilename = sanitizeFilename(asset.name)
@ -465,10 +456,7 @@ export function loader(): Loader {
/////////////////////////////////////////////////////////////////////////
export const group_by_cat = (items: IHowto[]) => {
return items.reduce((acc: Record<string, IHowto[]>, item: IHowto) => {
const category = item?.category?.label || 'uncategorized'
if (category === 'uncategorized') {
return acc
}
const category = item?.category?.label || 'Uncategorized'
if (!acc[category]) {
acc[category] = []
}

View File

@ -15,6 +15,7 @@ const items = (await getCollection(view)).map(
const groups = group_by_cat(items as any);
const locale = Astro.currentLocale;
const test = {};
console.log(Object.keys(groups))
export async function getStaticPaths() {
return LANGUAGES.map((lang) => ({
@ -46,8 +47,6 @@ export async function getStaticPaths() {
<ListItem
url={`/${locale}/howtos/${item.slug}`}
title={item.title}
price={item.price}
type={item.type}
alt={item.title}
model={{item:item}}
/>