UNPKG

scai

Version:

> **A local-first AI CLI for understanding, querying, and iterating on large codebases.** > **100% local • No token costs • No cloud • No prompt injection • Private by design**

188 lines (184 loc) 7.67 kB
// File: src/agents/steps/understandIntentStep.ts import { generate } from "../lib/generate.js"; import { logInputOutput } from "../utils/promptLogHelper.js"; import { extractFileReferences } from "../utils/extractFileReferences.js"; export const understandIntentStep = { name: "understandIntent", description: "Analyze the user query and determine its intent, type, and the appropriate task category.", /** * Run the step */ run: async (input) => { const { context } = input; const rawQuery = (context.initContext?.userQuery ?? "").trim(); // Fast-path for command-like probes (e.g. "/test") to avoid unnecessary model calls. if (rawQuery.startsWith("/")) { context.analysis ?? (context.analysis = {}); context.analysis.intent = { intent: "Handle command-like query input", intentCategory: "request", normalizedQuery: rawQuery, confidence: 0.9, targetFiles: extractFileReferences(rawQuery), targetSymbols: extractLikelySymbols(rawQuery), }; return; } const prompt = ` You are an AI assistant whose job is to determine the user's intent. User Query: ${context.initContext?.userQuery} Return a STRICT JSON object with the following fields: { "intent": "short sentence summarizing the user's intent", "intentCategory": "one of: question, request, codingTask, refactorTask, explanation, debugging, planning, writing, docsAndComments", "normalizedQuery": "a cleaned and direct restatement of the user query", "confidence": 0-1, // float "targetFiles": ["optional explicit filenames/paths from user query"], "targetSymbols": ["optional function/class/identifier targets from user query"] } Do not include commentary. Emit ONLY valid JSON. `.trim(); try { const genInput = { query: rawQuery, content: prompt }; const genOutput = await generate(genInput); let raw = genOutput.data; if (typeof raw !== "string") raw = JSON.stringify(raw ?? "{}"); logInputOutput("understandIntent", "output", raw); let parsed; try { parsed = JSON.parse(raw); } catch { parsed = { intent: "unknown", intentCategory: "", normalizedQuery: context.initContext?.userQuery, confidence: 0.3 }; } // --------------------- // Fallback for invalid / "other" categories // --------------------- let category = parsed.intentCategory?.trim(); if (!category || category.toLowerCase() === "other") { const q = parsed.normalizedQuery?.toLowerCase() ?? ""; if (q.includes("move") || q.includes("append") || q.includes("remove") || q.includes("insert")) { category = "refactorTask"; } else if (q.includes("comment") || q.includes("documentation") || q.includes("jsdoc") || q.includes("docstring")) { category = "docsAndComments"; } else if (q.includes("sentence") || q.includes("file")) { category = "explanation"; } else { category = "request"; // safe default } } category = normalizeIntentCategory(category); const intentConfidence = normalizeConfidence(parsed.confidence, 0.5); const llmTargetFiles = Array.isArray(parsed.targetFiles) ? parsed.targetFiles : []; const llmTargetSymbols = Array.isArray(parsed.targetSymbols) ? parsed.targetSymbols : []; // If model confidence is high and it already provided explicit targets, // avoid merging raw-query regex matches (pasted logs can add heavy noise). // Example: // - high confidence + targetFiles=["fileIndex.ts"] => keep only that target // - low confidence or no targets => merge heuristic extraction from raw query const shouldMergeHeuristics = intentConfidence < 0.6 || (llmTargetFiles.length === 0 && llmTargetSymbols.length === 0); const targetFiles = mergeUniqueCaseInsensitive(llmTargetFiles, shouldMergeHeuristics ? extractFileReferences(rawQuery) : []); const targetSymbols = mergeUniqueCaseInsensitive(llmTargetSymbols, shouldMergeHeuristics ? extractLikelySymbols(rawQuery) : []); // Ensure the analysis object exists context.analysis ?? (context.analysis = {}); // Store intent inside a dedicated object context.analysis.intent = { intent: parsed.intent, intentCategory: category, normalizedQuery: parsed.normalizedQuery ?? rawQuery, confidence: intentConfidence, targetFiles, targetSymbols, }; } catch (err) { console.error("understandIntent error:", err); // Ensure the analysis object exists context.analysis ?? (context.analysis = {}); context.analysis.intent = { intent: "unknown", intentCategory: "request", normalizedQuery: rawQuery, confidence: 0.0, targetFiles: extractFileReferences(rawQuery), targetSymbols: extractLikelySymbols(rawQuery), }; } } }; function mergeUniqueCaseInsensitive(primary, secondary) { const out = []; const seen = new Set(); for (const token of [...primary, ...secondary]) { if (typeof token !== "string") continue; const trimmed = token.trim(); if (!trimmed) continue; const key = trimmed.toLowerCase(); if (seen.has(key)) continue; seen.add(key); out.push(trimmed); } return out; } function extractLikelySymbols(query) { const matches = query.match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) ?? []; const out = new Set(); for (const token of matches) { const looksLikeSymbol = /[A-Z]/.test(token) || token.includes("_") || token.endsWith("Step") || token.endsWith("Module") || token.endsWith("Cmd"); if (!looksLikeSymbol) continue; out.add(token); } return Array.from(out); } function normalizeConfidence(value, fallback = 0.5) { if (typeof value !== "number" || !Number.isFinite(value)) { return fallback; } return Math.max(0, Math.min(1, value)); } function normalizeIntentCategory(raw) { const value = typeof raw === "string" ? raw.trim().toLowerCase() : ""; if (!value) return "request"; const directMap = { question: "question", request: "request", codingtask: "codingTask", refactortask: "refactorTask", explanation: "explanation", debugging: "debugging", planning: "planning", writing: "writing", docsandcomments: "docsAndComments", docsandcomment: "docsAndComments", docs: "docsAndComments", comments: "docsAndComments", comment: "docsAndComments", analysis: "question", analyze: "question", architecture: "explanation", }; return directMap[value] ?? "request"; }