promptix
v1.0.0
Published
Three-tier prompt loading with Redis caching and LRU cache support
Readme
Promptix (@sno/promptix)
Three-tier prompt loading system with Redis caching, user-specific prompts, and multilingual support.
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Promptix │
├─────────────────────────────────────────────────────────────────────┤
│ L1: LRU Cache (0.1ms) → L2: Redis (1-2ms) → L3: File (5-10ms) │
└─────────────────────────────────────────────────────────────────────┘Features:
- Dependency injection (no hardcoded config)
- Optional Redis (works file-only if unavailable)
- Pub/Sub cache invalidation
- User-specific prompts via
userId - Multilingual support (en, zh, es)
- Graceful fallback chain
Installation
# As npm package (private)
npm install @sno/promptix
# Or as git submodule
git submodule add https://github.com/sno-ai/promptix.git package/promptixPeer Dependencies:
{
"ioredis": "^5.0.0" // Optional - only needed for Redis features
}How to Use
Basic Usage
import { createPromptLoader } from "@sno/promptix";
// Create loader with file-only mode (no Redis)
const loader = createPromptLoader({
promptDir: "./prompts",
});
// Load English prompt
const prompt = await loader.loadPrompt("memory", "extract", 1);With Redis
const loader = createPromptLoader({
promptDir: "./prompts",
redisUrl: "redis://localhost:6379",
});
// Initialize Redis connection
await loader.initRedis();
// Start cache invalidation listener (optional)
await loader.startInvalidationListener();
// Load prompt
const prompt = await loader.loadPrompt("memory", "extract", 1);
// Cleanup on shutdown
await loader.close();With User-Specific Prompt
// Load user-specific prompt
const prompt = await loader.loadPrompt("memory", "extract", 1, {
userId: "user_12345",
});
// Falls back to generic prompt if user-specific not foundWith Language
// Load Chinese prompt
const prompt = await loader.loadPrompt("memory", "extract", 1, {
language: "zh",
});
// Falls back to English if Chinese not found
// Load Spanish prompt for specific user
const prompt = await loader.loadPrompt("memory", "extract", 1, {
userId: "user_12345",
language: "es",
});Full Options
const prompt = await loader.loadPrompt("memory", "extract", 2, {
fallbackVersion: 1, // Try v1 if v2 not found
context: "coding", // App context (default: "default")
userId: "user_123", // User-specific prompt
language: "zh", // Language (default: "en")
});Directory Structure
Filename Convention: {promptName}_v{version}.md (version suffix required)
{promptDir}/
├── {context}/ # Context (e.g., "default", "coding")
│ └── {category}/ # Category (e.g., "memory", "graph")
│ │
│ │── {promptName}_v{version}.md # English (root - no subdirectory)
│ │
│ ├── zh/ # Chinese (optional)
│ │ └── {promptName}_v{version}.md
│ │
│ ├── es/ # Spanish (optional)
│ │ └── {promptName}_v{version}.md
│ │
│ └── {userId}/ # User-specific (optional)
│ │── {promptName}_v{version}.md # User English
│ ├── zh/
│ │ └── {promptName}_v{version}.md # User Chinese
│ └── es/
│ └── {promptName}_v{version}.md # User SpanishExample Directory
prompts/
├── default/
│ ├── memory/
│ │ ├── extract_v1.md # English v1 (generic)
│ │ ├── extract_v2.md # English v2 (generic)
│ │ ├── zh/
│ │ │ └── extract_v1.md # Chinese v1
│ │ ├── es/
│ │ │ └── extract_v1.md # Spanish v1
│ │ └── user_12345/
│ │ ├── extract_v1.md # User-specific English v1
│ │ └── zh/
│ │ └── extract_v1.md # User-specific Chinese v1
│ │
│ └── graph/
│ └── build_v1.md
│
└── coding/ # Context for coding assistants
└── memory/
└── extract_v1.md # Coding-specific promptInput Parameters
createPromptLoader(config)
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| promptDir | string | Yes | - | Base directory for prompt files |
| redisUrl | string | No | - | Redis connection URL |
| cacheSize | number | No | 100 | LRU cache max entries |
| cacheTtlSeconds | number | No | 21600 | Local cache TTL (6 hours) |
| invalidationChannel | string | No | "prompt:invalidate" | Redis pub/sub channel |
| redisConnectTimeoutMs | number | No | 5000 | Redis connect timeout |
| redisCommandTimeoutMs | number | No | 5000 | Redis command timeout |
| redisMaxRetries | number | No | 3 | Max retries per request |
| logger | PromptLoaderLogger | No | console | Custom logger |
loadPrompt(category, promptName, version, options?)
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| category | string | Yes | - | Prompt category (e.g., "memory", "graph") |
| promptName | string | Yes | - | Prompt name without version (e.g., "extract") |
| version | number | Yes | - | Primary version to load (1-9999) |
| options.fallbackVersion | number | No | - | Version to try if primary not found |
| options.context | string | No | "default" | Application context |
| options.userId | string | No | - | User ID for user-specific prompts |
| options.language | "en" \| "zh" \| "es" | No | "en" | Language code |
Validation Rules
| Field | Pattern | Max Length | Notes |
|-------|---------|------------|-------|
| category | ^[a-z][a-z0-9_]{0,63}$ | 64 | Lowercase, alphanumeric, underscores |
| promptName | ^[a-z][a-z0-9_]{0,63}$ | 64 | Lowercase, alphanumeric, underscores |
| context | ^[a-z][a-z0-9_-]{0,63}$ | 64 | Also allows hyphens |
| userId | ^[a-zA-Z0-9_-]{1,64}$ | 64 | Alphanumeric, underscores, hyphens |
| version | Integer 1-9999 | - | - |
Fallback Chain
Prompts are searched in priority order until found:
With userId + language
1. {context}/{category}/{userId}/{lang}/{promptName}_v{version}.md
2. {context}/{category}/{userId}/{promptName}_v{version}.md # English fallback
3. {context}/{category}/{lang}/{promptName}_v{version}.md # Generic fallback
4. {context}/{category}/{promptName}_v{version}.md # English generic
5. default/{category}/{userId}/{lang}/{promptName}_v{version}.md # Default context
6. default/{category}/{userId}/{promptName}_v{version}.md
7. default/{category}/{lang}/{promptName}_v{version}.md
8. default/{category}/{promptName}_v{version}.mdWith language only (no userId)
1. {context}/{category}/{lang}/{promptName}_v{version}.md
2. {context}/{category}/{promptName}_v{version}.md # English fallback
3. default/{category}/{lang}/{promptName}_v{version}.md # Default context
4. default/{category}/{promptName}_v{version}.mdWith userId only (English)
1. {context}/{category}/{userId}/{promptName}_v{version}.md
2. {context}/{category}/{promptName}_v{version}.md # Generic fallback
3. default/{category}/{userId}/{promptName}_v{version}.md # Default context
4. default/{category}/{promptName}_v{version}.mdBasic (no userId, English)
1. {context}/{category}/{promptName}_v{version}.md
2. default/{category}/{promptName}_v{version}.md # Default contextOutput
loadPrompt() Return Value
Returns: Promise<string> - The prompt template content.
Cache Statistics
const stats = loader.getStats();Returns CacheStats:
{
localCache: {
size: 45, // Current cached items
maxSize: 100, // Max capacity
hits: 1250, // Cache hits
misses: 48, // Cache misses
hitRate: 96.3 // Hit rate percentage
},
redisAvailable: true, // Redis connection status
pubsubActive: true // Invalidation listener status
}Errors
| Error Class | When Thrown |
|-------------|-------------|
| PromptNotFoundError | Prompt file not found in any fallback path |
| InvalidPromptError | Template content invalid (empty, too short, too large) |
| SecurityError | Path traversal or dangerous characters detected |
Redis Key Format
prompt:{context}:{category}:{userId}:{lang}:{promptName}:v{version}Where:
userId="_"when no user specifiedlang="en"for English (always explicit in Redis)
Examples
prompt:default:memory:_:en:extract:v1 # Basic English
prompt:default:memory:_:zh:extract:v1 # Chinese
prompt:default:memory:user_123:en:extract:v1 # User-specific English
prompt:coding:memory:user_123:zh:extract:v2 # User-specific Chinese in coding contextCache Invalidation
Programmatic
// Clear all
loader.invalidate("*");
// Clear category
loader.invalidate("memory:*");
// Clear specific
loader.invalidate("default:memory:extract:v1");Via Redis Pub/Sub
# Publish invalidation message
redis-cli PUBLISH prompt:invalidate "memory:*"Best Practices
- Version Management: Start with v1, increment for breaking changes
- Fallback Versions: Always provide
fallbackVersionfor critical prompts - User-Specific Prompts: Use
userIdfor per-user fine-tuning, not user data storage - Languages: Only create localized files when translation is complete
- Context: Use for app modes (coding, chat), not user segmentation
Migration from subdirectory
The parameter subdirectory has been renamed to category for clarity:
// Before
loader.loadPrompt("memory_agent", "prompt", 1);
// After
loader.loadPrompt("memory", "extract", 1);Directory structure remains the same.
