This skill demonstrates how to use the para-obsidian LLM utilities for AI-assisted field suggestions in custom slash commands. It shows how to leverage the 3-layer architecture (constraints, prompt-builder, orchestration) to build intelligent metadata extraction that respects vault context and frontmatter rules.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill demonstrates how to use the para-obsidian LLM utilities for AI-assisted field suggestions in custom slash commands. It shows how to leverage the 3-layer architecture (constraints, prompt-builder, orchestration) to build intelligent metadata extraction that respects vault context and frontmatter rules.
The para-obsidian plugin provides a 3-layer LLM utility architecture in src/llm/:
constraints.ts)Deterministic extraction with enum/wikilink/vault context awareness:
buildConstraintSet(): Converts template + frontmatter rules into LLM constraintsprompt-builder.ts)Declarative, composable prompts:
buildStructuredPrompt(): Assembles system role, task, content, constraintsorchestration.ts)High-level workflows:
suggestFieldValues(): Single-field suggestionsconvertNoteToTemplate(): Full note conversion with validationcallOllama(): LLM integration with error handlingparseOllamaResponse(): Structured response parsingimport {
buildConstraintSet,
buildStructuredPrompt,
callOllama,
parseOllamaResponse,
type VaultContext
} from './llm';
import { getTemplate } from './templates';
import { loadConfig } from './config';
async function suggestProjectMetadata(
userTitle: string,
userDescription: string
): Promise<{ args: Record<string, unknown>; title: string }> {
// 1. Load config and template
const config = await loadConfig();
const template = getTemplate(config, 'project');
// 2. Build vault context (for wikilink validation)
const vaultContext: VaultContext = {
areas: ['Health', 'Career', 'Family'], // from Dataview or cache
resources: ['TypeScript', 'React'],
projects: ['Website Redesign'],
tags: ['#development', '#planning']
};
// 3. Build constraints from template + vault context
const constraints = buildConstraintSet(
template,
config.frontmatterRules,
vaultContext
);
// 4. Build structured prompt
const prompt = buildStructuredPrompt({
systemRole: 'Extract project metadata from user input following PARA method',
task: 'Suggest frontmatter field values based on title and description. Return ONLY valid JSON.',
sourceContent: `Title: ${userTitle}\nDescription: ${userDescription}`,
constraints
});
// 5. Call Ollama and parse response
const response = await callOllama(prompt, 'qwen2.5:7b');
const { args, title } = parseOllamaResponse(response);
return { args, title };
}
// In a slash command handler:
const suggestions = await suggestProjectMetadata(
'Build AI Assistant',
'Create a voice-controlled AI assistant using Whisper and Claude'
);
console.log('Suggested frontmatter:', suggestions.args);
// {
// area: '[[Career]]', // Wikilink format (Dataview compatible)
// status: 'active', // Enum value
// tags: ['#development', '#ai']
// }
// Present to user for confirmation before creating note
const confirmed = await promptUserConfirmation(suggestions);
if (confirmed) {
await createNote(suggestions.title, suggestions.args);
}
import { convertNoteToTemplate } from './llm';
import { loadConfig } from './config';
async function convertExistingNote(
noteContent: string,
targetTemplate: 'project' | 'area' | 'resource' | 'task'
): Promise<{ title: string; args: Record<string, unknown> }> {
const config = await loadConfig();
// Vault context for wikilink validation
const vaultContext = {
areas: await getExistingAreas(),
resources: await getExistingResources(),
projects: await getExistingProjects(),
tags: await getExistingTags()
};
// One-line conversion with full validation
const result = await convertNoteToTemplate(
noteContent,
targetTemplate,
config,
vaultContext,
'qwen2.5:7b'
);
return result;
}
// Example: Convert plain note to PARA project
const noteContent = `
# AI Voice Assistant
Working on a voice-controlled assistant using Whisper for transcription
and Claude for responses. This is part of my career development.
Status: Just started planning
Due: End of Q1
`;
const converted = await convertExistingNote(noteContent, 'project');
console.log(converted);
// {
// title: 'AI Voice Assistant',
// args: {
// area: '[[Career]]',
// status: 'planning',
// due_date: '2025-03-31',
// tags: ['#development', '#ai', '#voice']
// }
// }
import { suggestFieldValues } from './llm';
import { loadConfig } from './config';
// Suggest single field value (e.g., for interactive prompts)
async function suggestArea(projectTitle: string, projectDescription: string): Promise<string> {
const config = await loadConfig();
const vaultContext = {
areas: ['Health', 'Career', 'Family', 'Personal Growth']
};
const suggestion = await suggestFieldValues(
`Title: ${projectTitle}\nDescription: ${projectDescription}`,
'project',
config,
vaultContext,
'qwen2.5:7b'
);
return suggestion.args.area as string; // Returns '[[Career]]' in wikilink format
}
// Use in interactive command
const suggestedArea = await suggestArea(
'Learn TypeScript',
'Master TypeScript for career advancement'
);
console.log(`AI suggests area: ${suggestedArea}`);
// AI suggests area: [[Career]]
Before (Monolithic):
// Hard to maintain, error-prone, inconsistent
const prompt = `Extract metadata from this note. Use wikilinks for areas.
Available areas: Health, Career, Family.
Return JSON with area, status, tags.
Note: ${content}`;
After (Layered):
// Declarative, testable, reusable
const constraints = buildConstraintSet(template, rules, vaultContext);
const prompt = buildStructuredPrompt({ systemRole, task, content, constraints });
Advantages:
src/llm/*.test.ts)suggestFieldValues()Best for:
Example:
// Interactive project creation
const area = await suggestFieldValues(userInput, 'project', config, vault);
const confirmed = await prompt(`Use area: ${area.args.area}?`);
if (!confirmed) {
area.args.area = await manualAreaSelection();
}
convertNoteToTemplate()Best for:
Example:
// Batch convert all notes in a folder
for (const note of plainNotes) {
const converted = await convertNoteToTemplate(note.content, 'project', config, vault);
await updateNoteFrontmatter(note.path, converted.args);
}
import { parseOllamaResponse, callOllama } from './llm';
const response = await callOllama(prompt, model);
const { args, title } = parseOllamaResponse(response);
// Add custom validation
if (args.area && !vaultContext.areas.includes(args.area)) {
console.warn(`AI suggested non-existent area: ${args.area}`);
args.area = await promptUserForArea(); // Fallback to manual selection
}
// Add computed fields
args.created_date = new Date().toISOString().split('T')[0];
args.file_path = generateFilePath(title, args.area);
import { callOllama, parseOllamaResponse } from './llm';
let attempts = 0;
let result;
while (attempts < 3) {
try {
const response = await callOllama(prompt, model);
result = parseOllamaResponse(response);
// Validate critical fields
if (!result.args.area || !result.title) {
throw new Error('Missing required fields');
}
break; // Success
} catch (error) {
attempts++;
console.warn(`Attempt ${attempts} failed:`, error);
// Refine prompt for retry
prompt += '\nIMPORTANT: You must include both "title" and "area" fields.';
}
}
if (!result) {
throw new Error('Failed to extract metadata after 3 attempts');
}
/para-brain:ai-convert Command// commands/ai-convert.md
import { convertNoteToTemplate } from '../src/llm';
import { loadConfig } from '../src/config';
import { getVaultContext } from '../src/vault';
export async function aiConvertCommand(
notePath: string,
targetTemplate: 'project' | 'area' | 'resource' | 'task'
) {
const config = await loadConfig();
const vaultContext = await getVaultContext(config.vault_path);
const noteContent = await fs.readFile(notePath, 'utf-8');
const result = await convertNoteToTemplate(
noteContent,
targetTemplate,
config,
vaultContext,
'qwen2.5:7b'
);
console.log('\nAI Suggestions:');
console.log(`Title: ${result.title}`);
console.log(`Frontmatter:`, JSON.stringify(result.args, null, 2));
const confirmed = await promptConfirmation('Apply these changes?');
if (confirmed) {
await updateNoteFrontmatter(notePath, result.args);
console.log('✓ Note converted successfully');
}
}
import { describe, test, expect } from 'bun:test';
import { buildConstraintSet, buildStructuredPrompt } from './llm';
describe('AI Field Suggestions', () => {
test('builds constraints with vault context', () => {
const template = getTemplate(config, 'project');
const vaultContext = { areas: ['Career'], resources: [], projects: [], tags: [] };
const constraints = buildConstraintSet(template, config.frontmatterRules, vaultContext);
expect(constraints).toContain('area must be a wikilink from: [[Career]]');
expect(constraints).toContain('status must be one of: active, planning, on-hold, completed, archived');
});
test('assembles structured prompt correctly', () => {
const prompt = buildStructuredPrompt({
systemRole: 'Extract metadata',
task: 'Suggest frontmatter',
sourceContent: 'Title: Test Project',
constraints: ['area must be wikilink']
});
expect(prompt).toContain('Extract metadata');
expect(prompt).toContain('Title: Test Project');
expect(prompt).toContain('area must be wikilink');
});
});
qwen2.5:7b is fast, qwen2.5:32b is more accurateSource Files:
/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/llm/constraints.ts/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/llm/prompt-builder.ts/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/llm/orchestration.tsTests:
/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/llm/constraints.test.ts/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/llm/prompt-builder.test.ts/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/llm/orchestration.test.tsConfig:
/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/defaults.ts - Default frontmatter rules/Users/nathanvale/code/side-quest-marketplace/plugins/para-obsidian/src/config.ts - Config loadingsuggestFieldValues()