prepia 1.0.0

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/prepia.mjs +119 -0
  4. package/package.json +53 -0
  5. package/skill/SKILL.md +148 -0
  6. package/skill/config.json +29 -0
  7. package/src/analytics/dashboard.mjs +84 -0
  8. package/src/analytics/tracker.mjs +131 -0
  9. package/src/api/middleware.mjs +219 -0
  10. package/src/api/routes.mjs +142 -0
  11. package/src/api/server.mjs +150 -0
  12. package/src/cache/disk-store.mjs +199 -0
  13. package/src/cache/manager.mjs +142 -0
  14. package/src/cache/memory-store.mjs +205 -0
  15. package/src/chain/dag.mjs +209 -0
  16. package/src/chain/executor.mjs +103 -0
  17. package/src/chain/scheduler.mjs +89 -0
  18. package/src/client/adapters.mjs +483 -0
  19. package/src/client/connector.mjs +391 -0
  20. package/src/client/index.mjs +483 -0
  21. package/src/client/websocket.mjs +353 -0
  22. package/src/core/context-packager.mjs +169 -0
  23. package/src/core/engine.mjs +338 -0
  24. package/src/core/event-bus.mjs +84 -0
  25. package/src/core/prepimshot.mjs +120 -0
  26. package/src/core/task-decomposer.mjs +158 -0
  27. package/src/edge/lite.mjs +90 -0
  28. package/src/guard/checker.mjs +123 -0
  29. package/src/guard/fact-checker.mjs +105 -0
  30. package/src/guard/hallucination.mjs +108 -0
  31. package/src/index.mjs +67 -0
  32. package/src/models/local-model.mjs +171 -0
  33. package/src/models/provider.mjs +192 -0
  34. package/src/models/router.mjs +156 -0
  35. package/src/morph/optimizer.mjs +142 -0
  36. package/src/network/p2p.mjs +146 -0
  37. package/src/persona/detector.mjs +118 -0
  38. package/src/plugins/loader.mjs +120 -0
  39. package/src/plugins/registry.mjs +164 -0
  40. package/src/plugins/sandbox.mjs +79 -0
  41. package/src/rate/limiter.mjs +145 -0
  42. package/src/rate/shield.mjs +150 -0
  43. package/src/script/executor.mjs +164 -0
  44. package/src/script/parser.mjs +134 -0
  45. package/src/security/privacy.mjs +108 -0
  46. package/src/security/sanitizer.mjs +133 -0
  47. package/src/shadow/daemon.mjs +128 -0
  48. package/src/stream/handler.mjs +204 -0
  49. package/src/tools/calculator.mjs +312 -0
  50. package/src/tools/file-ops.mjs +138 -0
  51. package/src/tools/http-client.mjs +127 -0
  52. package/src/tools/orchestrator.mjs +205 -0
  53. package/src/tools/web-scraper.mjs +159 -0
  54. package/src/tools/web-search.mjs +129 -0
  55. package/src/vault/knowledge-base.mjs +207 -0
  56. package/src/vault/pattern-learner.mjs +192 -0
  57. package/workflows/analyze.json +32 -0
  58. package/workflows/automate.json +32 -0
  59. package/workflows/research.json +37 -0
  60. package/workflows/summarize.json +32 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @fileoverview Lightweight edge-compatible mode with minimal memory footprint.
3
+ * @module edge/lite
4
+ */
5
+
6
+ export class LiteMode {
7
+ /**
8
+ * @param {Object} [options]
9
+ * @param {boolean} [options.enableCache=true] - Enable caching
10
+ * @param {boolean} [options.enableAnalytics=false] - Disable analytics for speed
11
+ * @param {boolean} [options.enablePlugins=false] - Disable plugins
12
+ * @param {boolean} [options.enableP2P=false] - Disable P2P
13
+ */
14
+ constructor(options = {}) {
15
+ this._config = {
16
+ enableCache: options.enableCache ?? true,
17
+ enableAnalytics: options.enableAnalytics ?? false,
18
+ enablePlugins: options.enablePlugins ?? false,
19
+ enableP2P: options.enableP2P ?? false,
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Get the feature flags for lite mode.
25
+ * @returns {Object}
26
+ */
27
+ getConfig() {
28
+ return { ...this._config };
29
+ }
30
+
31
+ /**
32
+ * Check if a feature is enabled.
33
+ * @param {string} feature
34
+ * @returns {boolean}
35
+ */
36
+ isEnabled(feature) {
37
+ return this._config[feature] ?? false;
38
+ }
39
+
40
+ /**
41
+ * Create a minimal engine configuration for edge deployment.
42
+ * @returns {Object}
43
+ */
44
+ createMinimalConfig() {
45
+ return {
46
+ cache: {
47
+ memoryMaxSize: 100,
48
+ memoryTTL: 60000,
49
+ enableDisk: false,
50
+ },
51
+ rate: {
52
+ providers: {},
53
+ },
54
+ tools: ['calculator', 'web-search'],
55
+ analytics: this._config.enableAnalytics,
56
+ plugins: this._config.enablePlugins,
57
+ p2p: this._config.enableP2P,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Estimate memory usage for the current configuration.
63
+ * @returns {Object}
64
+ */
65
+ estimateMemory() {
66
+ let bytes = 1024; // Base overhead
67
+ if (this._config.enableCache) bytes += 50 * 1024; // ~50KB for cache structures
68
+ if (this._config.enableAnalytics) bytes += 20 * 1024;
69
+ if (this._config.enablePlugins) bytes += 30 * 1024;
70
+ if (this._config.enableP2P) bytes += 40 * 1024;
71
+ return {
72
+ estimatedBytes: bytes,
73
+ estimatedKB: Math.round(bytes / 1024),
74
+ estimatedMB: Math.round(bytes / (1024 * 1024) * 100) / 100,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Enable or disable a feature.
80
+ * @param {string} feature
81
+ * @param {boolean} enabled
82
+ */
83
+ setFeature(feature, enabled) {
84
+ if (feature in this._config) {
85
+ this._config[feature] = enabled;
86
+ }
87
+ }
88
+ }
89
+
90
+ export default LiteMode;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview Output quality verification.
3
+ * @module guard/checker
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} QualityScore
8
+ * @property {number} overall - Overall score (0-1)
9
+ * @property {number} completeness - How complete the answer is (0-1)
10
+ * @property {number} coherence - How coherent the text is (0-1)
11
+ * @property {number} relevance - How relevant to the query (0-1)
12
+ * @property {string[]} flags - Quality issues found
13
+ * @property {string[]} suggestions - Improvement suggestions
14
+ */
15
+
16
+ /**
17
+ * Check the quality of an LLM output.
18
+ * @param {string} output - The LLM output text
19
+ * @param {Object} [context]
20
+ * @param {string} [context.query] - Original query
21
+ * @param {number} [context.expectedLength] - Expected output length
22
+ * @returns {QualityScore}
23
+ */
24
+ export function checkQuality(output, context = {}) {
25
+ if (!output || typeof output !== 'string') {
26
+ return {
27
+ overall: 0,
28
+ completeness: 0,
29
+ coherence: 0,
30
+ relevance: 0,
31
+ flags: ['Empty or invalid output'],
32
+ suggestions: ['Provide a non-empty response'],
33
+ };
34
+ }
35
+
36
+ const flags = [];
37
+ const suggestions = [];
38
+ const trimmed = output.trim();
39
+
40
+ // Completeness check
41
+ let completeness = 1.0;
42
+ if (trimmed.length < 10) {
43
+ completeness -= 0.5;
44
+ flags.push('Output is very short');
45
+ suggestions.push('Provide a more detailed response');
46
+ }
47
+ if (trimmed.endsWith('...') || trimmed.endsWith('…')) {
48
+ completeness -= 0.3;
49
+ flags.push('Output appears truncated');
50
+ }
51
+ if (context.expectedLength && trimmed.length < context.expectedLength * 0.5) {
52
+ completeness -= 0.2;
53
+ flags.push('Output shorter than expected');
54
+ }
55
+
56
+ // Coherence check
57
+ let coherence = 1.0;
58
+ const sentences = trimmed.split(/[.!?]+/).filter(s => s.trim().length > 0);
59
+ if (sentences.length > 1) {
60
+ // Check for repetitive sentences
61
+ const unique = new Set(sentences.map(s => s.trim().toLowerCase()));
62
+ const repetitionRatio = unique.size / sentences.length;
63
+ if (repetitionRatio < 0.5) {
64
+ coherence -= 0.4;
65
+ flags.push('High repetition detected');
66
+ suggestions.push('Reduce repetitive content');
67
+ }
68
+ }
69
+ // Check for broken sentences
70
+ const brokenPattern = /\b(\w+)\s+\1\s+\1\b/g;
71
+ if (brokenPattern.test(trimmed)) {
72
+ coherence -= 0.3;
73
+ flags.push('Broken/repetitive text detected');
74
+ }
75
+
76
+ // Relevance check
77
+ let relevance = 0.8; // Default reasonable relevance
78
+ if (context.query) {
79
+ const queryWords = new Set(context.query.toLowerCase().split(/\s+/).filter(w => w.length > 3));
80
+ const outputWords = new Set(trimmed.toLowerCase().split(/\s+/));
81
+ let matches = 0;
82
+ for (const word of queryWords) {
83
+ if (outputWords.has(word)) matches++;
84
+ }
85
+ relevance = queryWords.size > 0 ? Math.min(1, matches / queryWords.size + 0.3) : 0.8;
86
+ }
87
+
88
+ // Check for common issues
89
+ if (/I (?:can't|cannot|am unable to|don't know)/i.test(trimmed)) {
90
+ flags.push('Output expresses uncertainty');
91
+ }
92
+ if (/sorry|apologize|apologies/i.test(trimmed) && trimmed.length < 100) {
93
+ completeness -= 0.3;
94
+ flags.push('Output is primarily an apology');
95
+ }
96
+
97
+ completeness = Math.max(0, Math.min(1, completeness));
98
+ coherence = Math.max(0, Math.min(1, coherence));
99
+ relevance = Math.max(0, Math.min(1, relevance));
100
+
101
+ const overall = (completeness * 0.4 + coherence * 0.3 + relevance * 0.3);
102
+
103
+ return {
104
+ overall: Math.round(overall * 100) / 100,
105
+ completeness: Math.round(completeness * 100) / 100,
106
+ coherence: Math.round(coherence * 100) / 100,
107
+ relevance: Math.round(relevance * 100) / 100,
108
+ flags,
109
+ suggestions,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Check if output passes minimum quality threshold.
115
+ * @param {string} output
116
+ * @param {number} [threshold=0.5]
117
+ * @returns {boolean}
118
+ */
119
+ export function passesThreshold(output, threshold = 0.5) {
120
+ return checkQuality(output).overall >= threshold;
121
+ }
122
+
123
+ export default { checkQuality, passesThreshold };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @fileoverview Cross-reference verification against cached knowledge.
3
+ * @module guard/fact-checker
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} FactCheckResult
8
+ * @property {boolean} verified - Whether claims could be verified
9
+ * @property {number} confidence - Confidence score (0-1)
10
+ * @property {Object[]} claims - Individual claim results
11
+ * @property {string[]} contradictions - Found contradictions
12
+ */
13
+
14
+ /**
15
+ * Extract factual claims from text.
16
+ * @param {string} text
17
+ * @returns {string[]}
18
+ */
19
+ export function extractClaims(text) {
20
+ if (!text) return [];
21
+ const claims = [];
22
+
23
+ // Sentences that look like factual claims
24
+ const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
25
+
26
+ for (const sentence of sentences) {
27
+ const trimmed = sentence.trim();
28
+ // Skip questions and opinions
29
+ if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should)\b/i.test(trimmed)) continue;
30
+ if (/^(i think|in my opinion|perhaps|maybe|it seems)/i.test(trimmed)) continue;
31
+
32
+ // Look for factual patterns
33
+ if (
34
+ /\d+/.test(trimmed) || // Contains numbers
35
+ /\b(is|are|was|were|has|have|had)\b/i.test(trimmed) || // State verbs
36
+ /\b(according to|research shows|studies indicate|data suggests)\b/i.test(trimmed) // Attribution
37
+ ) {
38
+ claims.push(trimmed);
39
+ }
40
+ }
41
+
42
+ return claims;
43
+ }
44
+
45
+ /**
46
+ * Verify claims against a knowledge base.
47
+ * @param {string} text - Text to fact-check
48
+ * @param {Object} knowledgeBase - Knowledge base instance
49
+ * @returns {Promise<FactCheckResult>}
50
+ */
51
+ export async function factCheck(text, knowledgeBase) {
52
+ const claims = extractClaims(text);
53
+ if (claims.length === 0) {
54
+ return { verified: true, confidence: 0.5, claims: [], contradictions: [] };
55
+ }
56
+
57
+ const results = [];
58
+ const contradictions = [];
59
+ let totalConfidence = 0;
60
+
61
+ for (const claim of claims) {
62
+ const keywords = claim.toLowerCase().split(/\s+/).filter(w => w.length > 4);
63
+ const searchResults = await knowledgeBase.search(keywords.join(' '), { limit: 3 });
64
+
65
+ let verified = false;
66
+ let confidence = 0;
67
+
68
+ if (searchResults.length > 0) {
69
+ // Check if any knowledge entry supports or contradicts the claim
70
+ for (const entry of searchResults) {
71
+ const entryText = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value);
72
+ const claimWords = new Set(claim.toLowerCase().split(/\s+/));
73
+ const entryWords = new Set(entryText.toLowerCase().split(/\s+/));
74
+
75
+ let overlap = 0;
76
+ for (const word of claimWords) {
77
+ if (entryWords.has(word) && word.length > 3) overlap++;
78
+ }
79
+
80
+ const similarity = overlap / Math.max(claimWords.size, 1);
81
+ if (similarity > 0.3) {
82
+ verified = true;
83
+ confidence = Math.min(1, similarity + (entry.confidence || 0.5) * 0.3);
84
+ }
85
+ }
86
+ }
87
+
88
+ // Heuristic confidence for unverified claims
89
+ if (!verified) {
90
+ confidence = 0.3; // Low confidence for unverifiable claims
91
+ }
92
+
93
+ totalConfidence += confidence;
94
+ results.push({ claim, verified, confidence });
95
+ }
96
+
97
+ return {
98
+ verified: results.every(r => r.verified),
99
+ confidence: claims.length > 0 ? totalConfidence / claims.length : 0.5,
100
+ claims: results,
101
+ contradictions,
102
+ };
103
+ }
104
+
105
+ export default { extractClaims, factCheck };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview Hallucination detection patterns.
3
+ * @module guard/hallucination
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} HallucinationResult
8
+ * @property {boolean} detected - Whether hallucination patterns were found
9
+ * @property {number} score - Hallucination likelihood (0-1, higher = more likely)
10
+ * @property {Object[]} flags - Specific issues found
11
+ */
12
+
13
+ /**
14
+ * Detect potential hallucinations in LLM output.
15
+ * @param {string} text - LLM output to check
16
+ * @returns {HallucinationResult}
17
+ */
18
+ export function detectHallucination(text) {
19
+ if (!text || typeof text !== 'string') {
20
+ return { detected: false, score: 0, flags: [] };
21
+ }
22
+
23
+ const flags = [];
24
+ let score = 0;
25
+
26
+ // Check for suspicious URLs
27
+ const urlPattern = /https?:\/\/[^\s]+/g;
28
+ const urls = text.match(urlPattern) || [];
29
+ for (const url of urls) {
30
+ // Check for obviously fake domains
31
+ if (/\.xyz\b|\.tk\b|\.ml\b|\.ga\b|\.cf\b/i.test(url)) {
32
+ flags.push({ type: 'suspicious_url', detail: `Suspicious TLD: ${url}` });
33
+ score += 0.3;
34
+ }
35
+ // Check for made-up-looking paths
36
+ if (/\/[a-z]{20,}\//i.test(url)) {
37
+ flags.push({ type: 'suspicious_url', detail: `Suspiciously long path: ${url}` });
38
+ score += 0.2;
39
+ }
40
+ }
41
+
42
+ // Check for fake statistics
43
+ const statPatterns = [
44
+ /(?:approximately|about|roughly|around)\s+\d+\.?\d*\s*%/gi,
45
+ /\d+\.?\d*\s*(?:percent|percentage)\s+of/gi,
46
+ /studies?\s+(?:show|indicate|suggest|found)\s+that\s+\d+/gi,
47
+ /according\s+to\s+(?:a\s+)?(?:recent|new|latest)\s+(?:study|research|report)/gi,
48
+ ];
49
+ for (const pattern of statPatterns) {
50
+ const matches = text.match(pattern) || [];
51
+ for (const match of matches) {
52
+ // Vague attribution is suspicious
53
+ if (/according to (?:a |the )?(?:recent|new|latest|some)/i.test(match)) {
54
+ flags.push({ type: 'vague_attribution', detail: match });
55
+ score += 0.2;
56
+ }
57
+ }
58
+ }
59
+
60
+ // Check for invented quotes
61
+ const quotePattern = /[""]([^""]{20,})[""](?:\s*[-–—]\s*(?:\w+)|(?:\s+said\s+))/g;
62
+ let quoteMatch;
63
+ while ((quoteMatch = quotePattern.exec(text)) !== null) {
64
+ // Quotes without proper attribution
65
+ if (!/(?:said|stated|wrote|according to|noted|explained)\s+\w+\s+\w+/.test(text.substring(Math.max(0, quoteMatch.index - 50), quoteMatch.index + quoteMatch[0].length + 50))) {
66
+ flags.push({ type: 'unverified_quote', detail: quoteMatch[0].substring(0, 80) });
67
+ score += 0.2;
68
+ }
69
+ }
70
+
71
+ // Check for overly specific numbers without sources
72
+ const specificStats = text.match(/\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b/g) || [];
73
+ if (specificStats.length > 3 && !/source|reference|study|report|data|census|survey/i.test(text)) {
74
+ flags.push({ type: 'unsourced_statistics', detail: `${specificStats.length} specific numbers without sources` });
75
+ score += 0.15;
76
+ }
77
+
78
+ // Check for hedging language that might indicate uncertainty
79
+ const hedging = (text.match(/\b(may|might|could|possibly|perhaps|allegedly|reportedly|supposedly)\b/gi) || []).length;
80
+ if (hedging > 5) {
81
+ flags.push({ type: 'excessive_hedging', detail: `${hedging} hedging words detected` });
82
+ score += 0.1;
83
+ }
84
+
85
+ // Check for contradictory statements
86
+ const contradictions = [
87
+ { positive: /\balways\b/gi, negative: /\bnever\b/gi },
88
+ { positive: /\bincrease[sd]?\b/gi, negative: /\bdecrease[sd]?\b/gi },
89
+ { positive: /\bmore\b/gi, negative: /\bless\b/gi },
90
+ ];
91
+ for (const { positive, negative } of contradictions) {
92
+ const posMatches = text.match(positive) || [];
93
+ const negMatches = text.match(negative) || [];
94
+ if (posMatches.length > 0 && negMatches.length > 2) {
95
+ flags.push({ type: 'potential_contradiction', detail: 'Contains both positive and negative qualifiers' });
96
+ score += 0.1;
97
+ break;
98
+ }
99
+ }
100
+
101
+ return {
102
+ detected: score > 0.3,
103
+ score: Math.min(1, score),
104
+ flags,
105
+ };
106
+ }
107
+
108
+ export default { detectHallucination };
package/src/index.mjs ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @fileoverview Prepia - AI middleware that reduces LLM quota usage by 80-95%.
3
+ * Main entry point and public API.
4
+ * @module prepia
5
+ */
6
+
7
+ export { PrepiaEngine } from './core/engine.mjs';
8
+ export { decompose, partitionTasks } from './core/task-decomposer.mjs';
9
+ export { packageContext, estimateTokens } from './core/context-packager.mjs';
10
+ export { generatePrompt, mergePrompts } from './core/prepimshot.mjs';
11
+ export { emit, on, once, off, removeAll } from './core/event-bus.mjs';
12
+
13
+ export { Orchestrator } from './tools/orchestrator.mjs';
14
+ export { search as webSearch } from './tools/web-search.mjs';
15
+ export { scrape as webScrape } from './tools/web-scraper.mjs';
16
+ export { evaluate as calculate } from './tools/calculator.mjs';
17
+ export * as fileOps from './tools/file-ops.mjs';
18
+ export * as httpClient from './tools/http-client.mjs';
19
+
20
+ export { CacheManager } from './cache/manager.mjs';
21
+ export { MemoryStore } from './cache/memory-store.mjs';
22
+ export { DiskStore } from './cache/disk-store.mjs';
23
+
24
+ export { ModelRouter } from './models/router.mjs';
25
+ export { classify, canHandleLocally, getLocalResponse, Complexity } from './models/local-model.mjs';
26
+ export * as provider from './models/provider.mjs';
27
+
28
+ export { RateShield } from './rate/shield.mjs';
29
+ export { TokenBucket, SlidingWindow } from './rate/limiter.mjs';
30
+
31
+ export { DAG } from './chain/dag.mjs';
32
+ export { Scheduler } from './chain/scheduler.mjs';
33
+ export { Executor } from './chain/executor.mjs';
34
+
35
+ export { KnowledgeBase } from './vault/knowledge-base.mjs';
36
+ export { PatternLearner } from './vault/pattern-learner.mjs';
37
+
38
+ export { PluginLoader } from './plugins/loader.mjs';
39
+ export { PluginRegistry } from './plugins/registry.mjs';
40
+ export { Sandbox } from './plugins/sandbox.mjs';
41
+
42
+ export { checkQuality, passesThreshold } from './guard/checker.mjs';
43
+ export { factCheck, extractClaims } from './guard/fact-checker.mjs';
44
+ export { detectHallucination } from './guard/hallucination.mjs';
45
+
46
+ export { detectPersona, getPersonas, getPersona } from './persona/detector.mjs';
47
+
48
+ export { Tracker } from './analytics/tracker.mjs';
49
+ export { generateReport, generateSummary } from './analytics/dashboard.mjs';
50
+
51
+ export { StreamHandler, Phases } from './stream/handler.mjs';
52
+
53
+ export { sanitize, validateParams, sanitizeOutput } from './security/sanitizer.mjs';
54
+ export { detectPII, redactPII, containsPII } from './security/privacy.mjs';
55
+
56
+ export { Daemon } from './shadow/daemon.mjs';
57
+ export { LiteMode } from './edge/lite.mjs';
58
+ export { Optimizer } from './morph/optimizer.mjs';
59
+ export { P2PNetwork } from './network/p2p.mjs';
60
+
61
+ export { parse as parseScript, validate as validateScript } from './script/parser.mjs';
62
+ export { ScriptExecutor } from './script/executor.mjs';
63
+
64
+ export { Server, createServer } from './api/server.mjs';
65
+
66
+ /** Version */
67
+ export const VERSION = '1.0.0';
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @fileoverview Lightweight local pattern matching / rule engine.
3
+ * Handles common queries without LLM calls.
4
+ * @module models/local-model
5
+ */
6
+
7
+ /**
8
+ * Query complexity levels.
9
+ * @enum {string}
10
+ */
11
+ export const Complexity = {
12
+ SIMPLE: 'simple',
13
+ MEDIUM: 'medium',
14
+ COMPLEX: 'complex',
15
+ };
16
+
17
+ /**
18
+ * Pattern definitions for local matching.
19
+ */
20
+ const PATTERNS = [
21
+ // Greetings
22
+ {
23
+ pattern: /^(hi|hello|hey|greetings|good\s+(morning|afternoon|evening))[\s!.]*$/i,
24
+ handler: () => ({ content: 'Hello! How can I help you?', type: 'greeting', complexity: Complexity.SIMPLE }),
25
+ },
26
+ // Time/date queries
27
+ {
28
+ pattern: /what\s+(?:time|date|day)\s+is\s+it|what(?:'s| is) (?:the )?(?:current )?(?:time|date|day)/i,
29
+ handler: () => {
30
+ const now = new Date();
31
+ return {
32
+ content: `Current date and time: ${now.toISOString()}`,
33
+ type: 'datetime',
34
+ complexity: Complexity.SIMPLE,
35
+ };
36
+ },
37
+ },
38
+ // Simple math
39
+ {
40
+ pattern: /^[\d\s+\-*/().%^]+$/,
41
+ handler: (query) => {
42
+ try {
43
+ // Simple arithmetic validation - must only contain math chars
44
+ const cleaned = query.replace(/\s/g, '');
45
+ if (!/^[\d+\-*/().%^]+$/.test(cleaned)) return null;
46
+ // Use Function constructor for safe eval (no access to globals)
47
+ const result = new Function(`"use strict"; return (${cleaned})`)();
48
+ if (typeof result === 'number' && isFinite(result)) {
49
+ return { content: String(result), type: 'math', complexity: Complexity.SIMPLE };
50
+ }
51
+ } catch {}
52
+ return null;
53
+ },
54
+ },
55
+ // Unit conversions
56
+ {
57
+ pattern: /(\d+\.?\d*)\s*(km|mi|miles?|meters?|feet|ft|inches?|in|cm|mm|kg|lbs?|pounds?|oz|ounces?|g|grams?|celsius|fahrenheit|°[cf])\s+(?:to|in|as)\s+(km|mi|miles?|meters?|feet|ft|inches?|in|cm|mm|kg|lbs?|pounds?|oz|ounces?|g|grams?|celsius|fahrenheit|°[cf])/i,
58
+ handler: () => null, // Handled by calculator
59
+ },
60
+ // Yes/no questions
61
+ {
62
+ pattern: /^(is|are|was|were|do|does|did|can|could|will|would|should|has|have)\s/i,
63
+ handler: (query) => ({
64
+ content: null, // Needs LLM
65
+ type: 'question',
66
+ complexity: Complexity.MEDIUM,
67
+ hint: 'This appears to be a factual question that may benefit from web search.',
68
+ }),
69
+ },
70
+ // Definitions
71
+ {
72
+ pattern: /^what(?:'s| is| are)\s+(?:a |an |the )?(.+)/i,
73
+ handler: (query, match) => ({
74
+ content: null,
75
+ type: 'definition',
76
+ topic: match[1],
77
+ complexity: Complexity.MEDIUM,
78
+ }),
79
+ },
80
+ // How-to
81
+ {
82
+ pattern: /^how\s+(?:do|to|can)\s+(.+)/i,
83
+ handler: (query, match) => ({
84
+ content: null,
85
+ type: 'howto',
86
+ topic: match[1],
87
+ complexity: Complexity.MEDIUM,
88
+ }),
89
+ },
90
+ // Summarize requests
91
+ {
92
+ pattern: /^(summarize|summarise|tldr|tl;dr|brief)\s/i,
93
+ handler: () => ({
94
+ content: null,
95
+ type: 'summarization',
96
+ complexity: Complexity.MEDIUM,
97
+ }),
98
+ },
99
+ ];
100
+
101
+ /**
102
+ * Classify and attempt to handle a query locally.
103
+ * @param {string} query - User query
104
+ * @returns {Object} Result with content (if handled locally), type, and complexity
105
+ */
106
+ export function classify(query) {
107
+ if (typeof query !== 'string') {
108
+ return { content: null, type: 'unknown', complexity: Complexity.COMPLEX };
109
+ }
110
+
111
+ const trimmed = query.trim();
112
+ if (trimmed.length === 0) {
113
+ return { content: '', type: 'empty', complexity: Complexity.SIMPLE };
114
+ }
115
+
116
+ for (const { pattern, handler } of PATTERNS) {
117
+ const match = trimmed.match(pattern);
118
+ if (match) {
119
+ const result = handler(trimmed, match);
120
+ if (result) return result;
121
+ }
122
+ }
123
+
124
+ // Default: needs LLM
125
+ return {
126
+ content: null,
127
+ type: 'general',
128
+ complexity: estimateComplexity(trimmed),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Estimate query complexity based on heuristics.
134
+ * @param {string} query
135
+ * @returns {string}
136
+ */
137
+ function estimateComplexity(query) {
138
+ const wordCount = query.split(/\s+/).length;
139
+ const hasMultipleQuestions = (query.match(/\?/g) || []).length > 1;
140
+ const hasConjunctions = /\b(and|but|or|however|although|because)\b/i.test(query);
141
+
142
+ if (wordCount < 10 && !hasMultipleQuestions && !hasConjunctions) {
143
+ return Complexity.SIMPLE;
144
+ }
145
+ if (wordCount > 30 || hasMultipleQuestions || (wordCount > 15 && hasConjunctions)) {
146
+ return Complexity.COMPLEX;
147
+ }
148
+ return Complexity.MEDIUM;
149
+ }
150
+
151
+ /**
152
+ * Check if a query can be fully handled locally (no LLM needed).
153
+ * @param {string} query
154
+ * @returns {boolean}
155
+ */
156
+ export function canHandleLocally(query) {
157
+ const result = classify(query);
158
+ return result.content !== null && result.content !== undefined;
159
+ }
160
+
161
+ /**
162
+ * Get a local response for a query (returns null if LLM needed).
163
+ * @param {string} query
164
+ * @returns {string|null}
165
+ */
166
+ export function getLocalResponse(query) {
167
+ const result = classify(query);
168
+ return result.content;
169
+ }
170
+
171
+ export default { classify, canHandleLocally, getLocalResponse, Complexity };