feat: sync open-source context and capsule removal
- remove legacy contexts surfaces from the app, MCP bridge, standalone server, and docs - keep getContext and retrieveQueryContext while aligning the simplified graph-only contract - harden dev startup migration behavior and disable the accidental chat surface in open source Generated with Codex
This commit is contained in:
@@ -15,7 +15,6 @@ export interface QuickAddInput {
|
||||
rawInput: string;
|
||||
mode?: QuickAddMode;
|
||||
description?: string;
|
||||
contextId?: number | null;
|
||||
}
|
||||
|
||||
export interface QuickAddResult {
|
||||
@@ -200,7 +199,7 @@ function isCreateNodeResponse(value: unknown): value is CreateNodeResponse {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleExtractionQuickAdd(type: ExtractionQuickAddType, url: string, task: string, contextId?: number | null): Promise<string> {
|
||||
async function handleExtractionQuickAdd(type: ExtractionQuickAddType, url: string, task: string): Promise<string> {
|
||||
const { toolName, execute } = EXTRACTION_TOOL_MAP[type];
|
||||
if (!execute) {
|
||||
throw new Error(`Tool ${toolName} does not have an execute function`);
|
||||
@@ -269,7 +268,6 @@ async function handleExtractionQuickAdd(type: ExtractionQuickAddType, url: strin
|
||||
refined_at: capturedAt,
|
||||
},
|
||||
},
|
||||
context_id: contextId,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -296,7 +294,7 @@ async function handleExtractionQuickAdd(type: ExtractionQuickAddType, url: strin
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNoteQuickAdd(rawInput: string, task: string, userDescription?: string, contextId?: number | null): Promise<string> {
|
||||
async function handleNoteQuickAdd(rawInput: string, task: string, userDescription?: string): Promise<string> {
|
||||
const content = rawInput.trim();
|
||||
if (!content) {
|
||||
throw new Error('Input is required to create a note');
|
||||
@@ -307,7 +305,6 @@ async function handleNoteQuickAdd(rawInput: string, task: string, userDescriptio
|
||||
const nodePayload: Record<string, unknown> = {
|
||||
title,
|
||||
source: content,
|
||||
context_id: contextId,
|
||||
metadata: {
|
||||
type: 'note',
|
||||
state: 'not_processed',
|
||||
@@ -357,7 +354,7 @@ async function handleNoteQuickAdd(rawInput: string, task: string, userDescriptio
|
||||
});
|
||||
}
|
||||
|
||||
async function handleChatTranscriptQuickAdd(rawInput: string, task: string, contextId?: number | null): Promise<string> {
|
||||
async function handleChatTranscriptQuickAdd(rawInput: string, task: string): Promise<string> {
|
||||
const transcript = rawInput.trim();
|
||||
if (!transcript) {
|
||||
throw new Error('Input is required to import a chat transcript');
|
||||
@@ -424,7 +421,6 @@ async function handleChatTranscriptQuickAdd(rawInput: string, task: string, cont
|
||||
title,
|
||||
description: nodeDescription,
|
||||
source: transcript,
|
||||
context_id: contextId,
|
||||
metadata,
|
||||
}),
|
||||
});
|
||||
@@ -451,7 +447,7 @@ async function handleChatTranscriptQuickAdd(rawInput: string, task: string, cont
|
||||
});
|
||||
}
|
||||
|
||||
export async function enqueueQuickAdd({ rawInput, mode, description, contextId }: QuickAddInput): Promise<QuickAddResult> {
|
||||
export async function enqueueQuickAdd({ rawInput, mode, description }: QuickAddInput): Promise<QuickAddResult> {
|
||||
const inputType = detectInputType(rawInput, mode);
|
||||
const task = buildTaskPrompt(inputType, rawInput);
|
||||
const id = `qa_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
@@ -468,11 +464,11 @@ export async function enqueueQuickAdd({ rawInput, mode, description, contextId }
|
||||
try {
|
||||
let summary: string;
|
||||
if (inputType === 'note') {
|
||||
summary = await handleNoteQuickAdd(rawInput, task, description, contextId);
|
||||
summary = await handleNoteQuickAdd(rawInput, task, description);
|
||||
} else if (inputType === 'chat') {
|
||||
summary = await handleChatTranscriptQuickAdd(rawInput, task, contextId);
|
||||
summary = await handleChatTranscriptQuickAdd(rawInput, task);
|
||||
} else {
|
||||
summary = await handleExtractionQuickAdd(inputType as ExtractionQuickAddType, rawInput, task, contextId);
|
||||
summary = await handleExtractionQuickAdd(inputType as ExtractionQuickAddType, rawInput, task);
|
||||
}
|
||||
|
||||
console.log(`[QuickAdd] Completed: ${task}`);
|
||||
|
||||
@@ -123,13 +123,6 @@ export function summarizeToolExecution(toolName: string, args: any, result: any)
|
||||
return 'No edges found.';
|
||||
}
|
||||
|
||||
if (toolName === 'writeContext') {
|
||||
const formatted = ensureString(result.data?.formatted_display);
|
||||
if (formatted) {
|
||||
return `Saved context as ${formatted}.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.data?.formatted_display) {
|
||||
return ensureString(result.data.formatted_display) || fallback;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function extractBearerToken(headerValue: string | null | undefined): string | null {
|
||||
if (!headerValue) return null;
|
||||
const parts = headerValue.split(' ');
|
||||
if (parts.length !== 2 || !/^Bearer$/i.test(parts[0] || '')) {
|
||||
return null;
|
||||
}
|
||||
return parts[1] || null;
|
||||
}
|
||||
|
||||
export function getCurrentSupabaseToken(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getInternalAuthHeaders(
|
||||
headers: Record<string, string> = {}
|
||||
): Record<string, string> {
|
||||
return { ...headers };
|
||||
}
|
||||
|
||||
export function applyRequestSupabaseAuth(_request: NextRequest): () => void {
|
||||
return () => {};
|
||||
}
|
||||
@@ -8,23 +8,6 @@ export interface AutoContextSummary {
|
||||
edgeCount: number;
|
||||
}
|
||||
|
||||
export interface ContextAnchorSummary {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
updatedAt: string;
|
||||
edgeCount: number;
|
||||
}
|
||||
|
||||
export interface PromptContextSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
count: number;
|
||||
anchor: ContextAnchorSummary | null;
|
||||
}
|
||||
|
||||
function truncate(value: string | null | undefined, maxChars: number): string {
|
||||
const trimmed = (value || '').trim();
|
||||
if (trimmed.length <= maxChars) return trimmed;
|
||||
@@ -67,128 +50,11 @@ function fetchAutoContextRows(limit: number): AutoContextSummary[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function getHubNodes(limit = 5): AutoContextSummary[] {
|
||||
export function getHubNodes(limit = 10): AutoContextSummary[] {
|
||||
return fetchAutoContextRows(limit);
|
||||
}
|
||||
|
||||
export function getContextSummaries(limit = 12): PromptContextSummary[] {
|
||||
const db = getSQLiteClient();
|
||||
const rows = db.query<{
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
count: number;
|
||||
anchor_id: number | null;
|
||||
anchor_title: string | null;
|
||||
anchor_description: string | null;
|
||||
anchor_updated_at: string | null;
|
||||
anchor_edge_count: number | null;
|
||||
}>(`
|
||||
WITH context_counts AS (
|
||||
SELECT c.id, c.name, c.description, c.icon, COUNT(n.id) AS count
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
GROUP BY c.id
|
||||
),
|
||||
ranked_anchors AS (
|
||||
SELECT
|
||||
c.id AS context_id,
|
||||
n.id AS node_id,
|
||||
n.title,
|
||||
n.description,
|
||||
n.updated_at,
|
||||
COUNT(e.id) AS edge_count,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY c.id
|
||||
ORDER BY COUNT(e.id) DESC, n.updated_at DESC, n.id ASC
|
||||
) AS anchor_rank
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
LEFT JOIN edges e ON (e.from_node_id = n.id OR e.to_node_id = n.id)
|
||||
GROUP BY c.id, n.id
|
||||
)
|
||||
SELECT
|
||||
cc.id,
|
||||
cc.name,
|
||||
cc.description,
|
||||
cc.icon,
|
||||
cc.count,
|
||||
ra.node_id AS anchor_id,
|
||||
ra.title AS anchor_title,
|
||||
ra.description AS anchor_description,
|
||||
ra.updated_at AS anchor_updated_at,
|
||||
ra.edge_count AS anchor_edge_count
|
||||
FROM context_counts cc
|
||||
LEFT JOIN ranked_anchors ra
|
||||
ON ra.context_id = cc.id
|
||||
AND ra.anchor_rank = 1
|
||||
ORDER BY cc.name COLLATE NOCASE ASC
|
||||
LIMIT ?
|
||||
`, [limit]).rows;
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description ?? null,
|
||||
icon: row.icon ?? null,
|
||||
count: Number(row.count ?? 0),
|
||||
anchor: row.anchor_id == null ? null : {
|
||||
id: Number(row.anchor_id),
|
||||
title: row.anchor_title || 'Untitled node',
|
||||
description: row.anchor_description || '',
|
||||
updatedAt: row.anchor_updated_at || '',
|
||||
edgeCount: Number(row.anchor_edge_count ?? 0),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildContextsBlock(limit = 12): string | null {
|
||||
const contexts = getContextSummaries(limit);
|
||||
if (contexts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
'User Contexts',
|
||||
'Contexts are optional soft hints. Use them when they are explicit and useful, but rely primarily on title, description, source, edges, and recency.',
|
||||
'',
|
||||
];
|
||||
|
||||
contexts.forEach((context, index) => {
|
||||
const description = truncate(context.description, 140) || 'No description.';
|
||||
const iconPrefix = context.icon ? `${context.icon} ` : '';
|
||||
lines.push(`${index + 1}. ${iconPrefix}${context.name} (${context.count} nodes)`);
|
||||
lines.push(` ${description}`);
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function buildContextAnchorsBlock(limit = 12): string | null {
|
||||
const contexts = getContextSummaries(limit).filter((context) => context.anchor);
|
||||
if (contexts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
'Context Anchors',
|
||||
'Each context anchor is the highest-edge node in that context. Use it only as an optional waypoint when that context is already clearly relevant.',
|
||||
'',
|
||||
];
|
||||
|
||||
contexts.forEach((context, index) => {
|
||||
const anchor = context.anchor!;
|
||||
lines.push(`${index + 1}. ${context.name}: [NODE:${anchor.id}:"${anchor.title}"] (${anchor.edgeCount} edges)`);
|
||||
if (anchor.description) {
|
||||
lines.push(` ${truncate(anchor.description, 160)}`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function buildHubNodesBlock(limit = 5): string | null {
|
||||
export function buildHubNodesBlock(limit = 10): string | null {
|
||||
const summaries = getHubNodes(limit);
|
||||
if (summaries.length === 0) {
|
||||
return null;
|
||||
@@ -210,10 +76,10 @@ export function buildHubNodesBlock(limit = 5): string | null {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function getAutoContextSummaries(limit = 5): AutoContextSummary[] {
|
||||
export function getAutoContextSummaries(limit = 10): AutoContextSummary[] {
|
||||
return getHubNodes(limit);
|
||||
}
|
||||
|
||||
export function buildAutoContextBlock(limit = 5): string | null {
|
||||
return buildContextsBlock(limit);
|
||||
export function buildAutoContextBlock(limit = 10): string | null {
|
||||
return buildHubNodesBlock(limit);
|
||||
}
|
||||
|
||||
@@ -463,7 +463,6 @@ export class ChunkService {
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
sqlite.disableFtsTable('chunks', 'chunks_fts query failed during chunk search', error);
|
||||
console.warn('[ChunkSearch] FTS chunk search failed, falling back to LIKE:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
import { getSQLiteClient } from './sqlite-client';
|
||||
import type { Context, ContextSummary, Node } from '@/types/database';
|
||||
import { nodeService } from './nodes';
|
||||
|
||||
type ContextRow = Context;
|
||||
export const MAX_CONTEXTS_PER_ACCOUNT = 10;
|
||||
|
||||
function normalizeContextName(name: string): string {
|
||||
return name.trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function assertContextName(name: unknown): string {
|
||||
if (typeof name !== 'string') {
|
||||
throw new Error('Context name is required.');
|
||||
}
|
||||
const normalized = normalizeContextName(name);
|
||||
if (!normalized) {
|
||||
throw new Error('Context name is required.');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function assertContextDescription(description: unknown): string {
|
||||
if (typeof description !== 'string') {
|
||||
throw new Error('Context description is required.');
|
||||
}
|
||||
const normalized = description.trim();
|
||||
if (!normalized) {
|
||||
throw new Error('Context description is required.');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapContextRow(row: ContextRow): Context {
|
||||
return {
|
||||
id: Number(row.id),
|
||||
name: row.name,
|
||||
description: row.description ?? null,
|
||||
icon: row.icon ?? null,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export class ContextService {
|
||||
async listContexts(): Promise<ContextSummary[]> {
|
||||
const sqlite = getSQLiteClient();
|
||||
const rows = sqlite.query<ContextSummary>(`
|
||||
SELECT c.id, c.name, c.description, c.icon, COUNT(n.id) as count
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name COLLATE NOCASE ASC
|
||||
`).rows;
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: Number(row.id),
|
||||
name: row.name,
|
||||
description: row.description ?? null,
|
||||
icon: row.icon ?? null,
|
||||
count: Number(row.count ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async getContextById(id: number): Promise<ContextSummary | null> {
|
||||
const sqlite = getSQLiteClient();
|
||||
const row = sqlite.query<ContextSummary>(`
|
||||
SELECT c.id, c.name, c.description, c.icon, COUNT(n.id) as count
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
WHERE c.id = ?
|
||||
GROUP BY c.id
|
||||
`, [id]).rows[0];
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
id: Number(row.id),
|
||||
name: row.name,
|
||||
description: row.description ?? null,
|
||||
icon: row.icon ?? null,
|
||||
count: Number(row.count ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getContextByName(name: string): Promise<Context | null> {
|
||||
const normalized = assertContextName(name);
|
||||
const sqlite = getSQLiteClient();
|
||||
const row = sqlite.query<ContextRow>(`
|
||||
SELECT id, name, description, icon, created_at, updated_at
|
||||
FROM contexts
|
||||
WHERE LOWER(TRIM(name)) = LOWER(TRIM(?))
|
||||
LIMIT 1
|
||||
`, [normalized]).rows[0];
|
||||
|
||||
return row ? mapContextRow(row) : null;
|
||||
}
|
||||
|
||||
async createContext(input: { name: string; description: string; icon?: string | null }): Promise<Context> {
|
||||
const name = assertContextName(input.name);
|
||||
const description = assertContextDescription(input.description);
|
||||
const icon = typeof input.icon === 'string' && input.icon.trim() ? input.icon.trim() : null;
|
||||
const sqlite = getSQLiteClient();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const existing = await this.getContextByName(name);
|
||||
if (existing) {
|
||||
throw new Error(`Context "${name}" already exists.`);
|
||||
}
|
||||
|
||||
const contextCountRow = sqlite.query<{ count: number }>(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM contexts
|
||||
`).rows[0];
|
||||
const existingCount = Number(contextCountRow?.count ?? 0);
|
||||
if (existingCount >= MAX_CONTEXTS_PER_ACCOUNT) {
|
||||
throw new Error(`Context limit reached. Maximum ${MAX_CONTEXTS_PER_ACCOUNT} contexts are allowed per account.`);
|
||||
}
|
||||
|
||||
const result = sqlite.prepare(`
|
||||
INSERT INTO contexts (name, description, icon, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(name, description, icon, now, now);
|
||||
|
||||
const created = await this.getContextById(Number(result.lastInsertRowid));
|
||||
if (!created) {
|
||||
throw new Error('Failed to create context.');
|
||||
}
|
||||
|
||||
return {
|
||||
...created,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
}
|
||||
|
||||
async updateContext(input: { id: number; name?: string; description?: string; icon?: string | null }): Promise<Context> {
|
||||
const sqlite = getSQLiteClient();
|
||||
const existing = sqlite.query<ContextRow>(`
|
||||
SELECT id, name, description, icon, created_at, updated_at
|
||||
FROM contexts
|
||||
WHERE id = ?
|
||||
`, [input.id]).rows[0];
|
||||
|
||||
if (!existing) {
|
||||
throw new Error(`Context ${input.id} not found.`);
|
||||
}
|
||||
|
||||
const nextName = input.name !== undefined ? assertContextName(input.name) : existing.name;
|
||||
const nextDescription = input.description !== undefined
|
||||
? assertContextDescription(input.description)
|
||||
: existing.description;
|
||||
const nextIcon = input.icon !== undefined
|
||||
? (typeof input.icon === 'string' && input.icon.trim() ? input.icon.trim() : null)
|
||||
: existing.icon;
|
||||
|
||||
const conflicting = sqlite.query<{ id: number }>(`
|
||||
SELECT id FROM contexts
|
||||
WHERE LOWER(TRIM(name)) = LOWER(TRIM(?))
|
||||
AND id != ?
|
||||
LIMIT 1
|
||||
`, [nextName, input.id]).rows[0];
|
||||
|
||||
if (conflicting) {
|
||||
throw new Error(`Context "${nextName}" already exists.`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
sqlite.prepare(`
|
||||
UPDATE contexts
|
||||
SET name = ?, description = ?, icon = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(nextName, nextDescription, nextIcon, now, input.id);
|
||||
|
||||
return {
|
||||
id: input.id,
|
||||
name: nextName,
|
||||
description: nextDescription ?? null,
|
||||
icon: nextIcon ?? null,
|
||||
created_at: existing.created_at,
|
||||
updated_at: now,
|
||||
};
|
||||
}
|
||||
|
||||
async getNodesForContext(id: number): Promise<Node[]> {
|
||||
return nodeService.getNodes({ contextId: id, limit: 500 });
|
||||
}
|
||||
|
||||
async resolveContextId(input: { context_id?: number | null; context_name?: string | null }): Promise<number | null | undefined> {
|
||||
const hasContextId =
|
||||
Object.prototype.hasOwnProperty.call(input, 'context_id') &&
|
||||
input.context_id !== undefined;
|
||||
const hasContextName = typeof input.context_name === 'string' && input.context_name.trim().length > 0;
|
||||
|
||||
if (!hasContextId && !hasContextName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasContextId && input.context_id === null) {
|
||||
if (hasContextName) {
|
||||
throw new Error('context_name cannot be combined with context_id: null.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let resolvedById: Context | null = null;
|
||||
if (hasContextId) {
|
||||
if (typeof input.context_id !== 'number' || !Number.isInteger(input.context_id) || input.context_id <= 0) {
|
||||
throw new Error('context_id must be a positive integer or null.');
|
||||
}
|
||||
const byId = await this.getContextById(input.context_id);
|
||||
if (!byId) {
|
||||
throw new Error(`Context ${input.context_id} not found.`);
|
||||
}
|
||||
resolvedById = {
|
||||
...byId,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
}
|
||||
|
||||
if (hasContextName) {
|
||||
const byName = await this.getContextByName(input.context_name!);
|
||||
if (!byName) {
|
||||
throw new Error(`Context "${input.context_name}" not found.`);
|
||||
}
|
||||
if (resolvedById && resolvedById.id !== byName.id) {
|
||||
throw new Error('context_id and context_name refer to different contexts.');
|
||||
}
|
||||
return byName.id;
|
||||
}
|
||||
|
||||
return resolvedById?.id;
|
||||
}
|
||||
}
|
||||
|
||||
export const contextService = new ContextService();
|
||||
@@ -4,7 +4,6 @@ import type { DatabaseIntegrityReport } from './sqlite-client';
|
||||
export { nodeService, NodeService } from './nodes';
|
||||
export { chunkService, ChunkService } from './chunks';
|
||||
export { edgeService, EdgeService } from './edges';
|
||||
export { contextService, ContextService } from './contextService';
|
||||
// export { HelperService } from './helpers'; // Removed - migrated to JSON-based service
|
||||
|
||||
// Types
|
||||
|
||||
@@ -2,12 +2,10 @@ import { getSQLiteClient } from './sqlite-client';
|
||||
import { Node, NodeFilters } from '@/types/database';
|
||||
import { eventBroadcaster } from '../events';
|
||||
import { EmbeddingService } from '@/services/embeddings';
|
||||
import { scoreNodeSearchMatch } from './searchRanking';
|
||||
import { getHighSignalSearchTerms, scoreNodeSearchMatch } from './searchRanking';
|
||||
import { buildCanonicalNodeMetadata, mergeNodeMetadata } from '@/services/nodes/metadata';
|
||||
|
||||
type NodeRow = Node & {
|
||||
context_json: string | null;
|
||||
};
|
||||
type NodeRow = Node;
|
||||
type NodeSearchRow = NodeRow & { rank?: number; similarity?: number };
|
||||
|
||||
function sanitizeFtsQuery(input: string): string {
|
||||
@@ -21,35 +19,7 @@ function sanitizeFtsQuery(input: string): string {
|
||||
}
|
||||
|
||||
function extractRelaxedSearchTerms(query: string): string[] {
|
||||
const stopWords = new Set([
|
||||
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'can', 'do', 'find',
|
||||
'for', 'from', 'hello', 'i', 'in', 'is', 'it', 'me', 'my', 'of', 'on',
|
||||
'or', 'recent', 'stuff', 'term', 'that', 'the', 'this', 'to', 'with', 'you'
|
||||
]);
|
||||
|
||||
const rawTerms = query
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]+/g, ' ')
|
||||
.split(/\s+/)
|
||||
.map(term => term.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const expanded = new Set<string>();
|
||||
|
||||
for (const term of rawTerms) {
|
||||
if (!stopWords.has(term) && term.length >= 3) {
|
||||
expanded.add(term);
|
||||
}
|
||||
|
||||
const alphaParts = term.replace(/\d+/g, ' ').split(/\s+/).filter(Boolean);
|
||||
for (const part of alphaParts) {
|
||||
if (!stopWords.has(part) && part.length >= 3) {
|
||||
expanded.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(expanded).slice(0, 8);
|
||||
return getHighSignalSearchTerms(query).slice(0, 8);
|
||||
}
|
||||
|
||||
function reciprocalRankFuse<T extends { id: number }>(
|
||||
@@ -90,7 +60,6 @@ export class NodeService {
|
||||
eventAfter,
|
||||
eventBefore,
|
||||
chunkStatus,
|
||||
contextId,
|
||||
} = filters;
|
||||
|
||||
if (search?.trim()) {
|
||||
@@ -112,8 +81,6 @@ export class NodeService {
|
||||
if (eventAfter) { query += ` AND n.event_date >= ?`; params.push(eventAfter); }
|
||||
if (eventBefore) { query += ` AND n.event_date < ?`; params.push(eventBefore); }
|
||||
if (chunkStatus) { query += ` AND n.chunk_status = ?`; params.push(chunkStatus); }
|
||||
if (contextId !== undefined) { query += ` AND n.context_id = ?`; params.push(contextId); }
|
||||
|
||||
const result = sqlite.query<{ total: number }>(query, params);
|
||||
return result.rows[0]?.total ?? 0;
|
||||
}
|
||||
@@ -131,7 +98,6 @@ export class NodeService {
|
||||
eventAfter,
|
||||
eventBefore,
|
||||
chunkStatus,
|
||||
contextId,
|
||||
} = filters;
|
||||
|
||||
if (search?.trim()) {
|
||||
@@ -143,14 +109,9 @@ export class NodeService {
|
||||
let query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json,
|
||||
n.created_at, n.updated_at,
|
||||
(SELECT COUNT(*) FROM edges WHERE from_node_id = n.id OR to_node_id = n.id) as edge_count
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
@@ -182,11 +143,6 @@ export class NodeService {
|
||||
query += ` AND n.chunk_status = ?`;
|
||||
params.push(chunkStatus);
|
||||
}
|
||||
if (contextId !== undefined) {
|
||||
query += ` AND n.context_id = ?`;
|
||||
params.push(contextId);
|
||||
}
|
||||
|
||||
// Sorting logic
|
||||
if (search) {
|
||||
// For search queries, prioritize by relevance: exact title → starts with → contains in title → description → source
|
||||
@@ -243,13 +199,8 @@ export class NodeService {
|
||||
const query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json
|
||||
n.created_at, n.updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE n.id = ?
|
||||
`;
|
||||
const result = sqlite.query<NodeRow>(query, [id]);
|
||||
@@ -275,7 +226,6 @@ export class NodeService {
|
||||
event_date,
|
||||
chunk_status,
|
||||
metadata = {},
|
||||
context_id,
|
||||
} = nodeData;
|
||||
const canonicalMetadata = buildCanonicalNodeMetadata({ metadata });
|
||||
const now = new Date().toISOString();
|
||||
@@ -284,8 +234,8 @@ export class NodeService {
|
||||
const nodeId = sqlite.transaction(() => {
|
||||
// Insert node using prepare/run for lastInsertRowid access
|
||||
const nodeResult = sqlite.prepare(`
|
||||
INSERT INTO nodes (title, description, source, link, event_date, metadata, chunk_status, context_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO nodes (title, description, source, link, event_date, metadata, chunk_status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
title,
|
||||
description ?? null,
|
||||
@@ -294,7 +244,6 @@ export class NodeService {
|
||||
event_date ?? null,
|
||||
JSON.stringify(canonicalMetadata),
|
||||
chunk_status ?? null,
|
||||
context_id ?? null,
|
||||
now,
|
||||
now
|
||||
);
|
||||
@@ -353,10 +302,6 @@ export class NodeService {
|
||||
if (source !== undefined) { setFields.push('source = ?'); params.push(source); }
|
||||
if (link !== undefined) { setFields.push('link = ?'); params.push(link); }
|
||||
if (event_date !== undefined) { setFields.push('event_date = ?'); params.push(event_date); }
|
||||
if (Object.prototype.hasOwnProperty.call(updates, 'context_id')) {
|
||||
setFields.push('context_id = ?');
|
||||
params.push(updates.context_id ?? null);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, 'chunk_status')) {
|
||||
setFields.push('chunk_status = ?');
|
||||
params.push(updates.chunk_status ?? null);
|
||||
@@ -419,11 +364,9 @@ export class NodeService {
|
||||
}
|
||||
|
||||
private mapNodeRow(row: NodeRow): Node {
|
||||
const { context_json, ...baseRow } = row;
|
||||
return {
|
||||
...baseRow,
|
||||
metadata: baseRow.metadata ? (typeof baseRow.metadata === 'string' ? JSON.parse(baseRow.metadata) : baseRow.metadata) : null,
|
||||
context: context_json ? JSON.parse(context_json) : null,
|
||||
...row,
|
||||
metadata: row.metadata ? (typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -433,7 +376,6 @@ export class NodeService {
|
||||
createdBefore,
|
||||
eventAfter,
|
||||
eventBefore,
|
||||
contextId,
|
||||
} = filters;
|
||||
|
||||
const clauses: string[] = [];
|
||||
@@ -443,8 +385,6 @@ export class NodeService {
|
||||
if (createdBefore) { clauses.push(`${alias}.created_at < ?`); params.push(createdBefore); }
|
||||
if (eventAfter) { clauses.push(`${alias}.event_date >= ?`); params.push(eventAfter); }
|
||||
if (eventBefore) { clauses.push(`${alias}.event_date < ?`); params.push(eventBefore); }
|
||||
if (contextId !== undefined) { clauses.push(`${alias}.context_id = ?`); params.push(contextId); }
|
||||
|
||||
return { clauses, params };
|
||||
}
|
||||
|
||||
@@ -517,7 +457,8 @@ export class NodeService {
|
||||
|
||||
return Number(result.rows[0]?.total ?? 0);
|
||||
} catch (error) {
|
||||
sqlite.disableFtsTable('nodes', 'nodes_fts query failed during count search', error);
|
||||
sqlite.getIntegrityReport(true);
|
||||
console.warn('[NodeSearch] FTS count failed, falling back to LIKE count:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +487,7 @@ export class NodeService {
|
||||
): NodeSearchRow[] {
|
||||
const ftsQuery = sanitizeFtsQuery(search);
|
||||
if (!ftsQuery) return [];
|
||||
|
||||
if (!sqlite.canUseFtsTable('nodes')) return [];
|
||||
|
||||
const { clauses, params } = this.buildNodeFilterClauses(filters);
|
||||
@@ -561,15 +503,10 @@ export class NodeService {
|
||||
)
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json,
|
||||
n.created_at, n.updated_at,
|
||||
fm.rank
|
||||
FROM fts_matches fm
|
||||
JOIN nodes n ON n.id = fm.rowid
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
${whereClauses}
|
||||
ORDER BY fm.rank
|
||||
LIMIT ?
|
||||
@@ -577,7 +514,8 @@ export class NodeService {
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
sqlite.disableFtsTable('nodes', 'nodes_fts query failed during node search', error);
|
||||
sqlite.getIntegrityReport(true);
|
||||
console.warn('[NodeSearch] FTS search failed, falling back to LIKE:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -593,13 +531,8 @@ export class NodeService {
|
||||
let query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json
|
||||
n.created_at, n.updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams = [...params];
|
||||
@@ -641,13 +574,8 @@ export class NodeService {
|
||||
let query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json
|
||||
n.created_at, n.updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams = [...params];
|
||||
@@ -718,15 +646,10 @@ export class NodeService {
|
||||
)
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json,
|
||||
n.created_at, n.updated_at,
|
||||
(1.0 / (1.0 + vm.distance)) AS similarity
|
||||
FROM vector_matches vm
|
||||
JOIN nodes n ON n.id = vm.node_id
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
${whereClauses}
|
||||
ORDER BY vm.distance
|
||||
LIMIT ?
|
||||
@@ -769,6 +692,14 @@ export class NodeService {
|
||||
return updatedNodes;
|
||||
}
|
||||
|
||||
async getAllDimensions(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDimensionStats(): Promise<{dimension: string, count: number}[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ export interface DatabaseEvent {
|
||||
| 'AGENT_DELEGATION_CREATED'
|
||||
| 'AGENT_DELEGATION_UPDATED'
|
||||
| 'GUIDE_UPDATED'
|
||||
| 'SKILL_UPDATED'
|
||||
| 'QUICK_ADD_COMPLETED'
|
||||
| 'QUICK_ADD_FAILED'
|
||||
| 'CONNECTION_ESTABLISHED';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { contextService } from '@/services/database/contextService';
|
||||
import { nodeService } from '@/services/database/nodes';
|
||||
import { countHighSignalQueryTermMatches, getHighSignalSearchTerms, scoreNodeSearchMatch } from '@/services/database/searchRanking';
|
||||
import type { Node } from '@/types/database';
|
||||
@@ -6,8 +5,6 @@ import type { Node } from '@/types/database';
|
||||
export interface DirectNodeLookupInput {
|
||||
search?: string;
|
||||
limit?: number;
|
||||
context_name?: string;
|
||||
contextId?: number;
|
||||
createdAfter?: string;
|
||||
createdBefore?: string;
|
||||
eventAfter?: string;
|
||||
@@ -20,7 +17,6 @@ export interface DirectNodeLookupResult {
|
||||
filtersApplied: {
|
||||
search?: string;
|
||||
limit: number;
|
||||
context_name?: string;
|
||||
createdAfter?: string;
|
||||
createdBefore?: string;
|
||||
eventAfter?: string;
|
||||
@@ -28,41 +24,6 @@ export interface DirectNodeLookupResult {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeContextName(value: string | undefined): string | undefined {
|
||||
if (typeof value !== 'string') return undefined;
|
||||
const normalized = value.trim().replace(/\s+/g, ' ');
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
async function resolveSearchContext(input: DirectNodeLookupInput): Promise<{ contextId?: number; context_name?: string }> {
|
||||
const normalizedName = normalizeContextName(input.context_name);
|
||||
if (normalizedName) {
|
||||
const context = await contextService.getContextByName(normalizedName);
|
||||
if (!context) {
|
||||
console.warn(`directNodeLookup received unknown context_name "${normalizedName}"; ignoring context filter.`);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
contextId: context.id,
|
||||
context_name: context.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof input.contextId === 'number') {
|
||||
const context = await contextService.getContextById(input.contextId);
|
||||
if (!context) {
|
||||
console.warn(`directNodeLookup received invalid legacy contextId ${input.contextId}; ignoring context filter.`);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
contextId: context.id,
|
||||
context_name: context.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function hasStrongAnchorMatch(nodes: Node[], searchTerm: string): boolean {
|
||||
if (!searchTerm || nodes.length === 0) return false;
|
||||
const highSignalTerms = getHighSignalSearchTerms(searchTerm);
|
||||
@@ -87,11 +48,9 @@ export async function directNodeLookup(input: DirectNodeLookupInput): Promise<Di
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedContext = await resolveSearchContext(input);
|
||||
const effectiveFilters = {
|
||||
search: searchTerm,
|
||||
limit,
|
||||
contextId: resolvedContext.contextId,
|
||||
searchMode: 'standard' as const,
|
||||
createdAfter: input.createdAfter,
|
||||
createdBefore: input.createdBefore,
|
||||
@@ -102,7 +61,6 @@ export async function directNodeLookup(input: DirectNodeLookupInput): Promise<Di
|
||||
let safeNodes = await nodeService.getNodes(effectiveFilters);
|
||||
|
||||
const hadExtraFilters = Boolean(
|
||||
effectiveFilters.contextId !== undefined ||
|
||||
effectiveFilters.createdAfter ||
|
||||
effectiveFilters.createdBefore ||
|
||||
effectiveFilters.eventAfter ||
|
||||
@@ -130,7 +88,6 @@ export async function directNodeLookup(input: DirectNodeLookupInput): Promise<Di
|
||||
filtersApplied: {
|
||||
search: searchTerm,
|
||||
limit,
|
||||
context_name: resolvedContext.context_name,
|
||||
createdAfter: input.createdAfter,
|
||||
createdBefore: input.createdBefore,
|
||||
eventAfter: input.eventAfter,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { chunkService } from '@/services/database/chunks';
|
||||
import { contextService } from '@/services/database/contextService';
|
||||
import { edgeService } from '@/services/database/edges';
|
||||
import { nodeService } from '@/services/database/nodes';
|
||||
import { countHighSignalQueryTermMatches, scoreNodeSearchMatch } from '@/services/database/searchRanking';
|
||||
@@ -33,7 +32,6 @@ export interface QueryContextResult {
|
||||
mode: 'skip' | 'focused' | 'query';
|
||||
reason: string;
|
||||
focused_node_id: number | null;
|
||||
active_context_id: number | null;
|
||||
nodes: RetrievedContextNode[];
|
||||
chunks: RetrievedContextChunk[];
|
||||
}
|
||||
@@ -41,7 +39,6 @@ export interface QueryContextResult {
|
||||
export interface RetrieveQueryContextInput {
|
||||
query: string;
|
||||
focused_node_id?: number | null;
|
||||
active_context_id?: number | null;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
@@ -104,7 +101,7 @@ function truncateText(value: string | null | undefined, maxLength = 180): string
|
||||
function queryTermCount(query: string): number {
|
||||
return normalizeWhitespace(query)
|
||||
.split(' ')
|
||||
.map((term) => term.trim())
|
||||
.map(term => term.trim())
|
||||
.filter(Boolean)
|
||||
.length;
|
||||
}
|
||||
@@ -161,7 +158,7 @@ function extractHighSignalTerms(query: string): string[] {
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]+/g, ' ')
|
||||
.split(/\s+/)
|
||||
.map((term) => singularizeTerm(term.trim()))
|
||||
.map(term => singularizeTerm(term.trim()))
|
||||
.filter(Boolean);
|
||||
|
||||
const seen = new Set<string>();
|
||||
@@ -184,8 +181,7 @@ function isLikelyUserNoteRecallQuery(query: string): boolean {
|
||||
|
||||
const explicitRecall = USER_RECALL_PATTERN.test(normalized);
|
||||
const firstPersonRecall = FIRST_PERSON_PATTERN.test(normalized) && /\bwhat\b/i.test(normalized);
|
||||
const explicitLookup = LOOKUP_PATTERN.test(normalized)
|
||||
&& (FIRST_PERSON_PATTERN.test(normalized) || /\b(created|saved|wrote|added)\b/i.test(normalized));
|
||||
const explicitLookup = LOOKUP_PATTERN.test(normalized) && (FIRST_PERSON_PATTERN.test(normalized) || /\b(created|saved|wrote|added)\b/i.test(normalized));
|
||||
const firstPersonShareRecall = FIRST_PERSON_SHARE_PATTERN.test(normalized);
|
||||
const noteHint = NOTE_HINT_PATTERN.test(normalized);
|
||||
const recentHint = RECENT_REFERENCE_PATTERN.test(normalized);
|
||||
@@ -211,8 +207,8 @@ function buildRecallSearchVariants(query: string): string[] {
|
||||
const terms = extractHighSignalTerms(query);
|
||||
if (terms.length === 0) return [];
|
||||
|
||||
const topicalTerms = terms.filter((term) => !NOTE_TERMS.has(term));
|
||||
const noteTerms = terms.filter((term) => NOTE_TERMS.has(term));
|
||||
const topicalTerms = terms.filter(term => !NOTE_TERMS.has(term));
|
||||
const noteTerms = terms.filter(term => NOTE_TERMS.has(term));
|
||||
const phraseVariants = extractRecallPhraseVariants(query);
|
||||
|
||||
const variants: string[] = [];
|
||||
@@ -281,8 +277,8 @@ function scoreRecallMatch(node: Node, query: string): number {
|
||||
if (normalizedSource.includes(normalizedPhrase)) score += 900;
|
||||
}
|
||||
|
||||
const titleTermMatches = terms.filter((term) => normalizedTitle.includes(term)).length;
|
||||
const totalTermMatches = terms.filter((term) => combined.includes(term)).length;
|
||||
const titleTermMatches = terms.filter(term => normalizedTitle.includes(term)).length;
|
||||
const totalTermMatches = terms.filter(term => combined.includes(term)).length;
|
||||
|
||||
score += titleTermMatches * 250;
|
||||
score += totalTermMatches * 120;
|
||||
@@ -298,7 +294,7 @@ function scoreRecallMatch(node: Node, query: string): number {
|
||||
}
|
||||
|
||||
function hasStrongRecallMatch(nodes: Node[], query: string): boolean {
|
||||
return nodes.some((node) => scoreRecallMatch(node, query) >= 1800);
|
||||
return nodes.some(node => scoreRecallMatch(node, query) >= 1800);
|
||||
}
|
||||
|
||||
function isLikelyUserAuthoredNote(node: Node): boolean {
|
||||
@@ -345,7 +341,7 @@ export function isFocusedSourceRequest(query: string): boolean {
|
||||
export function shouldRetrieveForQuery(query: string): boolean {
|
||||
const trimmed = normalizeWhitespace(query);
|
||||
if (!trimmed) return false;
|
||||
if (LOW_SIGNAL_PATTERNS.some((pattern) => pattern.test(trimmed))) return false;
|
||||
if (LOW_SIGNAL_PATTERNS.some(pattern => pattern.test(trimmed))) return false;
|
||||
|
||||
if (isFocusedSourceRequest(trimmed)) return true;
|
||||
if (SOURCE_DETAIL_PATTERN.test(trimmed)) return true;
|
||||
@@ -413,7 +409,6 @@ function rankRetrievedNodes(nodes: RetrievedContextNode[]): RetrievedContextNode
|
||||
export async function retrieveQueryContext(input: RetrieveQueryContextInput): Promise<QueryContextResult> {
|
||||
const query = normalizeWhitespace(input.query || '');
|
||||
const focusedNodeId = input.focused_node_id ?? null;
|
||||
const requestedActiveContextId = input.active_context_id ?? null;
|
||||
const limit = Math.min(Math.max(input.limit ?? 6, 1), 12);
|
||||
const shouldRetrieve = shouldRetrieveForQuery(query);
|
||||
|
||||
@@ -424,16 +419,11 @@ export async function retrieveQueryContext(input: RetrieveQueryContextInput): Pr
|
||||
mode: 'skip',
|
||||
reason: 'Query is too lightweight or conversational to justify retrieval.',
|
||||
focused_node_id: focusedNodeId,
|
||||
active_context_id: requestedActiveContextId,
|
||||
nodes: [],
|
||||
chunks: [],
|
||||
};
|
||||
}
|
||||
|
||||
const activeContextId = requestedActiveContextId
|
||||
? (await contextService.getContextById(requestedActiveContextId))?.id ?? null
|
||||
: null;
|
||||
|
||||
const focusedRequest = isFocusedSourceRequest(query);
|
||||
const nodesById = new Map<number, RetrievedContextNode>();
|
||||
|
||||
@@ -470,19 +460,6 @@ export async function retrieveQueryContext(input: RetrieveQueryContextInput): Pr
|
||||
});
|
||||
});
|
||||
|
||||
if (activeContextId && !strongRecallMatch) {
|
||||
const contextMatches = query
|
||||
? await nodeService.getNodes({ search: query, contextId: activeContextId, limit: Math.max(limit, 4) })
|
||||
: [];
|
||||
contextMatches.forEach((node, index) => {
|
||||
addNodeWithReason(nodesById, node, {
|
||||
kind: 'context_hint',
|
||||
reason: 'Also matched inside the active context.',
|
||||
searchRank: directQueryMatches.length + index,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!strongRecallMatch) {
|
||||
const rankedSeedNodes = rankRetrievedNodes(Array.from(nodesById.values())).slice(0, Math.max(3, limit));
|
||||
for (const seed of rankedSeedNodes.slice(0, 3)) {
|
||||
@@ -518,7 +495,6 @@ export async function retrieveQueryContext(input: RetrieveQueryContextInput): Pr
|
||||
? 'Direct node retrieval query: search the graph directly first and broaden only if needed.'
|
||||
: 'Substantive query: search the graph directly, then pull additional supporting context if helpful.',
|
||||
focused_node_id: focusedNodeId,
|
||||
active_context_id: activeContextId,
|
||||
nodes: finalNodes,
|
||||
chunks: chunks.map((chunk) => {
|
||||
const owner = finalNodes.find((node) => node.id === chunk.node_id) || directQueryMatches.find((node) => node.id === chunk.node_id);
|
||||
|
||||
@@ -19,7 +19,6 @@ interface NodeRecord {
|
||||
title: string;
|
||||
source: string | null;
|
||||
description: string | null;
|
||||
context_name: string | null;
|
||||
embedding?: Buffer | null;
|
||||
embedding_updated_at?: string | null;
|
||||
embedding_text?: string | null;
|
||||
@@ -57,7 +56,6 @@ export class NodeEmbedder {
|
||||
|
||||
Title: ${node.title}
|
||||
Source: ${node.source || 'No source'}
|
||||
Context: ${node.context_name || 'none'}
|
||||
|
||||
Focus on the main concepts, key relationships, and practical implications.`;
|
||||
|
||||
@@ -103,7 +101,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
||||
node.title,
|
||||
node.source || '',
|
||||
node.description,
|
||||
node.context_name
|
||||
null
|
||||
);
|
||||
|
||||
// Add AI analysis if source exists
|
||||
@@ -173,29 +171,26 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
||||
if (nodeId) {
|
||||
// Single node
|
||||
query = `
|
||||
SELECT n.id, n.title, n.source, n.description, c.name as context_name,
|
||||
SELECT n.id, n.title, n.source, n.description,
|
||||
n.embedding, n.embedding_updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE n.id = ?
|
||||
`;
|
||||
params = [nodeId];
|
||||
} else if (forceReEmbed) {
|
||||
// All nodes
|
||||
query = `
|
||||
SELECT n.id, n.title, n.source, n.description, c.name as context_name,
|
||||
SELECT n.id, n.title, n.source, n.description,
|
||||
n.embedding, n.embedding_updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
ORDER BY n.id
|
||||
`;
|
||||
} else {
|
||||
// Only nodes without embeddings
|
||||
query = `
|
||||
SELECT n.id, n.title, n.source, n.description, c.name as context_name,
|
||||
SELECT n.id, n.title, n.source, n.description,
|
||||
n.embedding, n.embedding_updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE n.embedding IS NULL OR n.embedding_updated_at IS NULL
|
||||
ORDER BY n.id
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user