notoken-core 1.6.0 → 2.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.
- package/config/chat-responses.json +767 -0
- package/config/concept-clusters.json +31 -0
- package/config/entities.json +93 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +4946 -83
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +424 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/conversation/coreference.js +44 -4
- package/dist/conversation/pendingActions.d.ts +55 -0
- package/dist/conversation/pendingActions.js +127 -0
- package/dist/conversation/store.d.ts +72 -0
- package/dist/conversation/store.js +140 -1
- package/dist/conversation/topicTracker.d.ts +36 -0
- package/dist/conversation/topicTracker.js +141 -0
- package/dist/execution/ssh.d.ts +42 -1
- package/dist/execution/ssh.js +532 -3
- package/dist/handlers/executor.js +3981 -16
- package/dist/index.d.ts +25 -3
- package/dist/index.js +36 -2
- package/dist/nlp/batchParser.d.ts +30 -0
- package/dist/nlp/batchParser.js +77 -0
- package/dist/nlp/conceptExpansion.d.ts +54 -0
- package/dist/nlp/conceptExpansion.js +136 -0
- package/dist/nlp/conceptRouter.d.ts +49 -0
- package/dist/nlp/conceptRouter.js +302 -0
- package/dist/nlp/confidenceCalibrator.d.ts +62 -0
- package/dist/nlp/confidenceCalibrator.js +116 -0
- package/dist/nlp/correctionLearner.d.ts +45 -0
- package/dist/nlp/correctionLearner.js +207 -0
- package/dist/nlp/entitySpellCorrect.d.ts +35 -0
- package/dist/nlp/entitySpellCorrect.js +141 -0
- package/dist/nlp/knowledgeGraph.d.ts +70 -0
- package/dist/nlp/knowledgeGraph.js +380 -0
- package/dist/nlp/llmFallback.js +28 -1
- package/dist/nlp/multiClassifier.js +91 -6
- package/dist/nlp/multiIntent.d.ts +43 -0
- package/dist/nlp/multiIntent.js +154 -0
- package/dist/nlp/parseIntent.d.ts +6 -1
- package/dist/nlp/parseIntent.js +180 -5
- package/dist/nlp/ruleParser.js +315 -0
- package/dist/nlp/semanticSimilarity.d.ts +30 -0
- package/dist/nlp/semanticSimilarity.js +174 -0
- package/dist/nlp/vocabularyBuilder.d.ts +43 -0
- package/dist/nlp/vocabularyBuilder.js +224 -0
- package/dist/nlp/wikidata.d.ts +49 -0
- package/dist/nlp/wikidata.js +228 -0
- package/dist/policy/confirm.d.ts +10 -0
- package/dist/policy/confirm.js +39 -0
- package/dist/policy/safety.js +6 -4
- package/dist/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- package/dist/utils/browser.d.ts +64 -0
- package/dist/utils/browser.js +364 -0
- package/dist/utils/commandHistory.d.ts +20 -0
- package/dist/utils/commandHistory.js +108 -0
- package/dist/utils/completer.d.ts +17 -0
- package/dist/utils/completer.js +79 -0
- package/dist/utils/config.js +32 -2
- package/dist/utils/dbQuery.d.ts +25 -0
- package/dist/utils/dbQuery.js +248 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +826 -0
- package/dist/utils/diskCleanup.d.ts +36 -0
- package/dist/utils/diskCleanup.js +775 -0
- package/dist/utils/entityResolver.d.ts +107 -0
- package/dist/utils/entityResolver.js +468 -0
- package/dist/utils/imageGen.d.ts +92 -0
- package/dist/utils/imageGen.js +2031 -0
- package/dist/utils/installTracker.d.ts +57 -0
- package/dist/utils/installTracker.js +160 -0
- package/dist/utils/multiExec.d.ts +21 -0
- package/dist/utils/multiExec.js +141 -0
- package/dist/utils/openclawDiag.d.ts +29 -0
- package/dist/utils/openclawDiag.js +1035 -0
- package/dist/utils/output.js +4 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/progressReporter.d.ts +50 -0
- package/dist/utils/progressReporter.js +58 -0
- package/dist/utils/projectDetect.d.ts +44 -0
- package/dist/utils/projectDetect.js +319 -0
- package/dist/utils/projectScanner.d.ts +44 -0
- package/dist/utils/projectScanner.js +312 -0
- package/dist/utils/shellCompat.d.ts +78 -0
- package/dist/utils/shellCompat.js +186 -0
- package/dist/utils/smartArchive.d.ts +16 -0
- package/dist/utils/smartArchive.js +172 -0
- package/dist/utils/smartRetry.d.ts +26 -0
- package/dist/utils/smartRetry.js +114 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- package/package.json +6 -3
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-intent parser.
|
|
3
|
+
*
|
|
4
|
+
* Splits compound sentences into individual intents and creates a plan.
|
|
5
|
+
*
|
|
6
|
+
* "check if the firewall is blocking port 443 and also check dns for my domain"
|
|
7
|
+
* → Step 1: firewall.list (check port 443)
|
|
8
|
+
* Step 2: dns.lookup (check domain)
|
|
9
|
+
*
|
|
10
|
+
* "show me disk usage, check memory, and list running containers"
|
|
11
|
+
* → Step 1: server.check_disk
|
|
12
|
+
* Step 2: server.check_memory
|
|
13
|
+
* Step 3: docker.list
|
|
14
|
+
*
|
|
15
|
+
* Splitting rules:
|
|
16
|
+
* - Split on: "and", "also", "then", "after that", ",", ";"
|
|
17
|
+
* - But NOT inside quoted strings or after "and" that joins nouns ("cats and dogs")
|
|
18
|
+
* - Each part is parsed independently through rule parser + concept router
|
|
19
|
+
* - Only creates a plan if 2+ distinct intents are found
|
|
20
|
+
*/
|
|
21
|
+
import { parseByRules } from "./ruleParser.js";
|
|
22
|
+
import { routeByConcepts } from "./conceptRouter.js";
|
|
23
|
+
import { getIntentDef } from "../utils/config.js";
|
|
24
|
+
const c = {
|
|
25
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
26
|
+
green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m",
|
|
27
|
+
};
|
|
28
|
+
// ─── Sentence Splitting ────────────────────────────────────────────────────
|
|
29
|
+
// Conjunctions that typically join separate commands
|
|
30
|
+
const SPLIT_PATTERNS = [
|
|
31
|
+
/\s+and\s+(?:also\s+)?(?:then\s+)?(?:can\s+you\s+)?/i,
|
|
32
|
+
/\s+also\s+/i,
|
|
33
|
+
/\s+then\s+/i,
|
|
34
|
+
/\s+after\s+that\s+/i,
|
|
35
|
+
/\s*;\s*/,
|
|
36
|
+
/\s*,\s+(?:and\s+)?(?:also\s+)?(?:then\s+)?(?:can\s+you\s+)?/i,
|
|
37
|
+
/\s+but\s+(?:first\s+)?(?:also\s+)?/i,
|
|
38
|
+
];
|
|
39
|
+
// Don't split on "and" that joins nouns (e.g., "cats and dogs", "videos and photos")
|
|
40
|
+
const NOUN_AND_PATTERN = /^[a-z]+\s+and\s+[a-z]+$/i;
|
|
41
|
+
/**
|
|
42
|
+
* Split a compound sentence into parts.
|
|
43
|
+
*/
|
|
44
|
+
export function splitCompoundSentence(text) {
|
|
45
|
+
let parts = [text.trim()];
|
|
46
|
+
for (const pattern of SPLIT_PATTERNS) {
|
|
47
|
+
const newParts = [];
|
|
48
|
+
for (const part of parts) {
|
|
49
|
+
const splits = part.split(pattern).map(s => s.trim()).filter(s => s.length > 2);
|
|
50
|
+
if (splits.length > 1) {
|
|
51
|
+
// Check if this is just noun joining (don't split "videos and photos")
|
|
52
|
+
const isNounJoin = splits.length === 2 && NOUN_AND_PATTERN.test(part);
|
|
53
|
+
if (isNounJoin) {
|
|
54
|
+
newParts.push(part);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
newParts.push(...splits);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
newParts.push(part);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
parts = newParts;
|
|
65
|
+
}
|
|
66
|
+
// Clean up: remove leading filler words
|
|
67
|
+
return parts.map(p => p.replace(/^(can you|could you|please|will you|would you)\s+/i, "").trim()).filter(p => p.length > 2);
|
|
68
|
+
}
|
|
69
|
+
// ─── Multi-Intent Parsing ──────────────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Parse a potentially compound sentence into a multi-step plan.
|
|
72
|
+
* Returns a single-step plan if only one intent is found.
|
|
73
|
+
*/
|
|
74
|
+
export function parseMultiIntent(rawText) {
|
|
75
|
+
const parts = splitCompoundSentence(rawText);
|
|
76
|
+
// If only one part, it's a single intent
|
|
77
|
+
if (parts.length <= 1) {
|
|
78
|
+
const intent = resolveIntent(rawText);
|
|
79
|
+
return {
|
|
80
|
+
steps: intent ? [intentToStep(intent, rawText)] : [],
|
|
81
|
+
originalText: rawText,
|
|
82
|
+
isSingleIntent: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Parse each part independently
|
|
86
|
+
const steps = [];
|
|
87
|
+
const seenIntents = new Set();
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
const intent = resolveIntent(part);
|
|
90
|
+
if (intent && !seenIntents.has(intent.intent)) {
|
|
91
|
+
steps.push(intentToStep(intent, part));
|
|
92
|
+
seenIntents.add(intent.intent);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
steps,
|
|
97
|
+
originalText: rawText,
|
|
98
|
+
isSingleIntent: steps.length <= 1,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function resolveIntent(text) {
|
|
102
|
+
// Try rule parser first
|
|
103
|
+
const rule = parseByRules(text);
|
|
104
|
+
if (rule && rule.confidence >= 0.6)
|
|
105
|
+
return rule;
|
|
106
|
+
// Then concept router
|
|
107
|
+
const concept = routeByConcepts(text);
|
|
108
|
+
if (concept && concept.confidence >= 0.5) {
|
|
109
|
+
return {
|
|
110
|
+
intent: concept.intent,
|
|
111
|
+
rawText: text,
|
|
112
|
+
confidence: concept.confidence,
|
|
113
|
+
fields: {},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function intentToStep(intent, rawText) {
|
|
119
|
+
const def = getIntentDef(intent.intent);
|
|
120
|
+
return {
|
|
121
|
+
intent: intent.intent,
|
|
122
|
+
rawText,
|
|
123
|
+
confidence: intent.confidence,
|
|
124
|
+
description: def?.description ?? intent.intent,
|
|
125
|
+
requiresConfirmation: def?.requiresConfirmation ?? false,
|
|
126
|
+
riskLevel: def?.riskLevel ?? "low",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ─── Plan Formatting ───────────────────────────────────────────────────────
|
|
130
|
+
export function formatPlanSteps(plan) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
if (plan.steps.length === 0) {
|
|
133
|
+
return `${c.dim}Could not create a plan from: "${plan.originalText}"${c.reset}`;
|
|
134
|
+
}
|
|
135
|
+
if (plan.isSingleIntent) {
|
|
136
|
+
return ""; // Don't show plan for single intents
|
|
137
|
+
}
|
|
138
|
+
const hasWrite = plan.steps.some(s => s.requiresConfirmation || s.riskLevel !== "low");
|
|
139
|
+
lines.push(`${c.bold}${c.cyan}Plan (${plan.steps.length} steps):${c.reset}`);
|
|
140
|
+
if (hasWrite) {
|
|
141
|
+
lines.push(`${c.yellow}⚠ Some steps modify your system — confirmation required${c.reset}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push("");
|
|
144
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
145
|
+
const step = plan.steps[i];
|
|
146
|
+
const num = `${c.cyan}${i + 1}.${c.reset}`;
|
|
147
|
+
const risk = step.riskLevel !== "low" ? ` ${c.yellow}[${step.riskLevel}]${c.reset}` : "";
|
|
148
|
+
const confirm = step.requiresConfirmation ? ` ${c.yellow}(needs confirmation)${c.reset}` : "";
|
|
149
|
+
lines.push(` ${num} ${c.bold}${step.intent}${c.reset}${risk}${confirm}`);
|
|
150
|
+
lines.push(` ${c.dim}${step.description}${c.reset}`);
|
|
151
|
+
lines.push(` ${c.dim}"${step.rawText}"${c.reset}`);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { ParsedCommand } from "../types/intent.js";
|
|
2
|
-
|
|
2
|
+
import { type MultiIntentPlan } from "./multiIntent.js";
|
|
3
|
+
/** Result from parseIntent — may contain a multi-step plan */
|
|
4
|
+
export type { MultiIntentPlan };
|
|
5
|
+
export declare function parseIntent(rawText: string): Promise<ParsedCommand & {
|
|
6
|
+
plan?: MultiIntentPlan;
|
|
7
|
+
}>;
|
package/dist/nlp/parseIntent.js
CHANGED
|
@@ -2,22 +2,194 @@ import { parseByRules } from "./ruleParser.js";
|
|
|
2
2
|
import { parseByLLM } from "./llmParser.js";
|
|
3
3
|
import { disambiguate } from "./disambiguate.js";
|
|
4
4
|
import { logFailure } from "../utils/logger.js";
|
|
5
|
+
import { classifyMulti } from "./multiClassifier.js";
|
|
6
|
+
import { lookupUnknownNouns } from "./wikidata.js";
|
|
7
|
+
import { routeByConcepts } from "./conceptRouter.js";
|
|
8
|
+
import { parseMultiIntent } from "./multiIntent.js";
|
|
9
|
+
import { isAffirmation, consumePendingAction, isRedirectingPendingAction } from "../conversation/pendingActions.js";
|
|
5
10
|
export async function parseIntent(rawText) {
|
|
6
|
-
// Stage
|
|
11
|
+
// Stage -2: Knowledge graph reference resolution
|
|
12
|
+
// Only resolve if coreference hasn't already handled it (avoid double resolution).
|
|
13
|
+
// Coreference runs in interactive mode (interactive.ts), knowledge graph here covers one-shot mode.
|
|
14
|
+
// Stage -2: Knowledge graph reference resolution with candidate scoring
|
|
15
|
+
const hasPronouns = /\b(it|that|this)\b/i.test(rawText) && !/\b(it's|that's|this is)\b/i.test(rawText);
|
|
16
|
+
if (hasPronouns) {
|
|
17
|
+
try {
|
|
18
|
+
const { resolveCandidates } = await import("./knowledgeGraph.js");
|
|
19
|
+
const { getOrCreateConversation, getRecentEntities } = await import("../conversation/store.js");
|
|
20
|
+
const conv = getOrCreateConversation(process.cwd());
|
|
21
|
+
const recentEnts = getRecentEntities(conv, 5).map((e) => e.entity);
|
|
22
|
+
const words = rawText.split(/\s+/);
|
|
23
|
+
let resolved = rawText;
|
|
24
|
+
for (const word of words) {
|
|
25
|
+
if (/^(it|that|this)$/i.test(word)) {
|
|
26
|
+
const candidates = resolveCandidates(word, recentEnts);
|
|
27
|
+
if (candidates.length > 0) {
|
|
28
|
+
const best = candidates[0];
|
|
29
|
+
// Only resolve if confident (score > 0.5) or clear winner (gap > 0.2)
|
|
30
|
+
const gap = candidates.length > 1 ? best.score - candidates[1].score : 1;
|
|
31
|
+
if (best.score > 0.5 || gap > 0.2) {
|
|
32
|
+
resolved = resolved.replace(new RegExp(`\\b${word}\\b`, "i"), best.entity.name);
|
|
33
|
+
// Show candidates if close (for transparency)
|
|
34
|
+
if (candidates.length > 1 && gap < 0.3) {
|
|
35
|
+
const alt = candidates.slice(0, 3).map(c => `${c.entity.name} (${(c.score * 100).toFixed(0)}%)`).join(", ");
|
|
36
|
+
console.error(`\x1b[2mResolved "${word}" → ${best.entity.name} (candidates: ${alt})\x1b[0m`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (resolved !== rawText) {
|
|
43
|
+
const resolvedResult = parseByRules(resolved);
|
|
44
|
+
if (resolvedResult && resolvedResult.confidence >= 0.7) {
|
|
45
|
+
resolvedResult.rawText = rawText;
|
|
46
|
+
return disambiguate(resolvedResult);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { /* knowledge graph not available */ }
|
|
51
|
+
}
|
|
52
|
+
// Stage -1a: check if user is redirecting a pending action ("put it on F drive")
|
|
53
|
+
const redirect = isRedirectingPendingAction(rawText);
|
|
54
|
+
if (redirect) {
|
|
55
|
+
consumePendingAction();
|
|
56
|
+
// Re-parse with the new location context
|
|
57
|
+
const reParsed = parseByRules(redirect);
|
|
58
|
+
if (reParsed && reParsed.confidence >= 0.5)
|
|
59
|
+
return disambiguate(reParsed);
|
|
60
|
+
// Fall through to normal parsing with the redirected text
|
|
61
|
+
rawText = redirect;
|
|
62
|
+
}
|
|
63
|
+
// Stage -1b: check if user is affirming a pending action ("ok", "try it", "do it")
|
|
64
|
+
if (isAffirmation(rawText)) {
|
|
65
|
+
const pending = consumePendingAction();
|
|
66
|
+
if (pending) {
|
|
67
|
+
if (pending.type === "intent") {
|
|
68
|
+
const reParsed = parseByRules(pending.action);
|
|
69
|
+
if (reParsed && reParsed.confidence >= 0.5) {
|
|
70
|
+
return disambiguate(reParsed);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return disambiguate({
|
|
74
|
+
intent: pending.action.includes(".") ? pending.action : "unknown",
|
|
75
|
+
rawText: pending.action,
|
|
76
|
+
confidence: 0.8,
|
|
77
|
+
fields: pending.fields ?? {},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Stage 0: check for compound sentences (multi-intent)
|
|
82
|
+
// "check disk and show me containers and list crontabs"
|
|
83
|
+
const multiPlan = parseMultiIntent(rawText);
|
|
84
|
+
if (!multiPlan.isSingleIntent && multiPlan.steps.length >= 2) {
|
|
85
|
+
// Check if one step is just a location modifier (not a real separate command)
|
|
86
|
+
// "do it offline and put files on D drive" → single intent with drive modifier
|
|
87
|
+
const locationStep = multiPlan.steps.find(s => /\b(put|place|install|store)\b.*\b(on|in|at)\s+[a-z]\s*drive\b/i.test(s.rawText)
|
|
88
|
+
|| /\b(on|in|at)\s+\/\S+/i.test(s.rawText));
|
|
89
|
+
const actionStep = multiPlan.steps.find(s => s !== locationStep);
|
|
90
|
+
if (locationStep && actionStep && multiPlan.steps.length === 2) {
|
|
91
|
+
// Merge: use the action step's intent but the full original text
|
|
92
|
+
// so the executor can extract the drive from "on D drive"
|
|
93
|
+
const result = disambiguate({
|
|
94
|
+
intent: actionStep.intent,
|
|
95
|
+
rawText, // full original text
|
|
96
|
+
confidence: actionStep.confidence,
|
|
97
|
+
fields: {},
|
|
98
|
+
});
|
|
99
|
+
return result; // single intent, no plan
|
|
100
|
+
}
|
|
101
|
+
// Real multi-intent plan
|
|
102
|
+
const firstStep = multiPlan.steps[0];
|
|
103
|
+
const result = disambiguate({
|
|
104
|
+
intent: firstStep.intent,
|
|
105
|
+
rawText, // full original text
|
|
106
|
+
confidence: firstStep.confidence,
|
|
107
|
+
fields: {},
|
|
108
|
+
});
|
|
109
|
+
return { ...result, plan: multiPlan };
|
|
110
|
+
}
|
|
111
|
+
// Stage 1: deterministic rule parser (synonym matching + spell correction)
|
|
7
112
|
const ruleResult = parseByRules(rawText);
|
|
8
113
|
if (ruleResult && ruleResult.confidence >= 0.7) {
|
|
9
114
|
return disambiguate(ruleResult);
|
|
10
115
|
}
|
|
11
|
-
// Stage 2:
|
|
116
|
+
// Stage 2: concept router — understands topics/domains, not just phrases
|
|
117
|
+
// Handles: "is this offline or cloud", "check my crontabs", etc.
|
|
118
|
+
const conceptResult = routeByConcepts(rawText);
|
|
119
|
+
if (conceptResult && conceptResult.confidence >= 0.6) {
|
|
120
|
+
return disambiguate({
|
|
121
|
+
intent: conceptResult.intent,
|
|
122
|
+
rawText,
|
|
123
|
+
confidence: conceptResult.confidence,
|
|
124
|
+
fields: {},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Stage 2.5: multi-classifier (synonym + semantic + vector + fuzzy voting)
|
|
128
|
+
const multiResult = classifyMulti(rawText);
|
|
129
|
+
if (multiResult.best && multiResult.best.score >= 0.6 && !multiResult.ambiguous) {
|
|
130
|
+
const mFields = ruleResult?.fields ?? {};
|
|
131
|
+
return disambiguate({
|
|
132
|
+
intent: multiResult.best.intent,
|
|
133
|
+
rawText,
|
|
134
|
+
confidence: Math.min(0.95, multiResult.best.score),
|
|
135
|
+
fields: mFields,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Stage 2.75: semantic similarity — catches paraphrases that exact matching misses
|
|
139
|
+
try {
|
|
140
|
+
const { findSimilarIntents } = await import("./semanticSimilarity.js");
|
|
141
|
+
const similar = findSimilarIntents(rawText, 3);
|
|
142
|
+
if (similar.length > 0 && similar[0].score >= 0.4) {
|
|
143
|
+
// Only use if it's clearly the best match (gap > 0.1 from second)
|
|
144
|
+
const gap = similar.length > 1 ? similar[0].score - similar[1].score : 1;
|
|
145
|
+
if (gap > 0.08 || similar[0].score >= 0.6) {
|
|
146
|
+
return disambiguate({
|
|
147
|
+
intent: similar[0].intent,
|
|
148
|
+
rawText,
|
|
149
|
+
confidence: Math.min(0.85, similar[0].score + 0.3),
|
|
150
|
+
fields: {},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { /* semantic similarity not available */ }
|
|
156
|
+
// Stage 3: LLM fallback
|
|
12
157
|
const llmResult = await parseByLLM(rawText);
|
|
13
158
|
if (llmResult && llmResult.confidence >= 0.5) {
|
|
14
159
|
return disambiguate(llmResult);
|
|
15
160
|
}
|
|
16
|
-
// Stage
|
|
161
|
+
// Stage 4: if rule parser had a low-confidence result, use it anyway
|
|
17
162
|
if (ruleResult) {
|
|
18
163
|
return disambiguate(ruleResult);
|
|
19
164
|
}
|
|
20
|
-
// Stage 4:
|
|
165
|
+
// Stage 4: check if the input looks like a "what is X" knowledge query
|
|
166
|
+
// If it contains unknown nouns, try Wikidata lookup and route to knowledge.lookup
|
|
167
|
+
const looksLikeQuestion = /^(what|who|tell|define|explain|info|facts|learn)\b/i.test(rawText.trim());
|
|
168
|
+
if (looksLikeQuestion) {
|
|
169
|
+
const topic = rawText.replace(/^(what|who)\s+(is|are|was|were)\s+/i, "")
|
|
170
|
+
.replace(/^(tell me about|define|explain|info about|facts about|learn about)\s+/i, "")
|
|
171
|
+
.replace(/\?$/, "").trim();
|
|
172
|
+
if (topic.length >= 2) {
|
|
173
|
+
return disambiguate({
|
|
174
|
+
intent: "knowledge.lookup",
|
|
175
|
+
rawText,
|
|
176
|
+
confidence: 0.6,
|
|
177
|
+
fields: { topic },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Stage 5: for completely unknown input, try to identify unknown nouns via Wikidata
|
|
182
|
+
// and attach the context to the unknown result so the user gets useful info
|
|
183
|
+
const words = rawText.toLowerCase().split(/\s+/);
|
|
184
|
+
let wikiContext;
|
|
185
|
+
try {
|
|
186
|
+
const entities = await lookupUnknownNouns(words);
|
|
187
|
+
if (entities.length > 0) {
|
|
188
|
+
wikiContext = entities.map(e => `${e.label}: ${e.description}`).join("; ");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
// Stage 6: unknown — log the failure for auto-learning
|
|
21
193
|
logFailure({
|
|
22
194
|
rawText,
|
|
23
195
|
timestamp: new Date().toISOString(),
|
|
@@ -29,6 +201,9 @@ export async function parseIntent(rawText) {
|
|
|
29
201
|
intent: "unknown",
|
|
30
202
|
rawText,
|
|
31
203
|
confidence: 0,
|
|
32
|
-
fields: {
|
|
204
|
+
fields: {
|
|
205
|
+
reason: "No supported intent matched",
|
|
206
|
+
...(wikiContext ? { wikiContext } : {}),
|
|
207
|
+
},
|
|
33
208
|
});
|
|
34
209
|
}
|