Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ai/config/ai-models.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"semanticExtraction": {
"provider": "onnx",
"model": "gliner2-relex",
"version": "1.0",
"modelId": "dx111ge/gliner2-multi-v1-onnx",
"path": "models/gliner2-relex",
"confidenceThreshold": 0.60,
"defaultEntityTypes": [
"Database", "Framework", "Software Component", "Microcontroller",
"Device", "Module", "Integration", "Broker", "Architecture",
"Model", "Service", "Person", "Application", "Concept"
],
"defaultRelationTypes": [
"USES", "STORES", "GENERATES", "CREATES", "COMMUNICATES_WITH",
"CONTROLS", "CONNECTS_TO", "INTEGRATES_WITH", "DEPENDS_ON", "IMPLEMENTS"
]
}
}
115 changes: 115 additions & 0 deletions ai/context/GraphRetriever.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,121 @@ class GraphRetriever {
}
}

/**
* Find entities by ontology type
*/
findEntitiesByType(type, limit = 20) {
if (!this.graphDB) return [];
try {
const db = this.graphDB.db || this.graphDB;
if (!db || typeof db.prepare !== 'function') return [];
const stmt = db.prepare('SELECT id, name, canonical_name, type, note_path FROM entities WHERE LOWER(type) = LOWER(?) OR LOWER(ontology_class) = LOWER(?) LIMIT ?');
return stmt.all(type, type, limit);
} catch {
return [];
}
}

/**
* Explain path between two entities
*/
explainPath(entityAName, entityBName) {
if (!this.graphDB) return null;
try {
const db = this.graphDB.db || this.graphDB;
if (!db || typeof db.prepare !== 'function') return null;

const entA = db.prepare('SELECT id, name FROM entities WHERE LOWER(name) = LOWER(?) OR note_path = ? LIMIT 1').get(entityAName, entityAName);
const entB = db.prepare('SELECT id, name FROM entities WHERE LOWER(name) = LOWER(?) OR note_path = ? LIMIT 1').get(entityBName, entityBName);

if (!entA || !entB) return null;

let pathIds = null;
if (typeof this.graphDB.findPath === 'function') {
pathIds = this.graphDB.findPath(entA.id, entB.id);
}
if (!pathIds || pathIds.length < 2) return `${entA.name} and ${entB.name} are not connected in the graph.`;

const names = pathIds.map(id => {
const row = db.prepare('SELECT name FROM entities WHERE id = ?').get(id);
return row?.name || id;
});

return names.join(' --> ');
} catch (err) {
log.warn('Failed explainPath:', err.message);
return null;
}
}

/**
* Get workspace summary
*/
getWorkspaceSummary() {
if (!this.graphDB) return null;
try {
let workspaceEntity = null;
if (typeof this.graphDB.getWorkspaceEntity === 'function') {
workspaceEntity = this.graphDB.getWorkspaceEntity();
}
const nodeCount = typeof this.graphDB.getNodeCount === 'function' ? this.graphDB.getNodeCount() : 0;
const edgeCount = typeof this.graphDB.getEdgeCount === 'function' ? this.graphDB.getEdgeCount() : 0;

return {
workspace: workspaceEntity || { name: 'Workspace' },
stats: { nodeCount, edgeCount }
};
} catch {
return null;
}
}

/**
* Get dependencies for an entity
*/
getDependencies(entityName, direction = 'both') {
if (!this.graphDB) return [];
try {
const db = this.graphDB.db || this.graphDB;
if (!db || typeof db.prepare !== 'function') return [];

const ent = db.prepare('SELECT id, name FROM entities WHERE LOWER(name) = LOWER(?) OR note_path = ? LIMIT 1').get(entityName, entityName);
if (!ent) return [];

let query = '';
if (direction === 'outgoing') {
query = "SELECT e.name as target, r.type FROM relationships r JOIN entities e ON r.target_id = e.id WHERE r.source_id = ? AND r.type IN ('depends_on', 'uses', 'imports', 'connects_to')";
} else if (direction === 'incoming') {
query = "SELECT e.name as source, r.type FROM relationships r JOIN entities e ON r.source_id = e.id WHERE r.target_id = ? AND r.type IN ('depends_on', 'uses', 'imports', 'connects_to')";
} else {
query = "SELECT e_src.name as source, r.type, e_tgt.name as target FROM relationships r JOIN entities e_src ON r.source_id = e_src.id JOIN entities e_tgt ON r.target_id = e_tgt.id WHERE (r.source_id = ? OR r.target_id = ?) AND r.type IN ('depends_on', 'uses', 'imports', 'connects_to')";
}

const stmt = db.prepare(query);
return direction === 'both' ? stmt.all(ent.id, ent.id) : stmt.all(ent.id);
} catch {
return [];
}
}

/**
* Get related documents for an entity
*/
getRelatedDocuments(entityName) {
if (!this.graphDB) return [];
try {
const rows = this.traverse(entityName, 2);
const paths = new Set();
rows.forEach(r => {
if (r.from_path && r.from_path.endsWith('.md')) paths.add(r.from_path);
if (r.to_path && r.to_path.endsWith('.md')) paths.add(r.to_path);
});
return Array.from(paths);
} catch {
return [];
}
}

/**
* Vercel AI SDK tool definition for this retriever.
*/
Expand Down
2 changes: 1 addition & 1 deletion ai/core/AIConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class AIConfig {
enablePatternLearning: true,
enableEmbeddings: true,
enableRelationshipDiscovery: true,
graphProvider: 'gliner-glirel',
graphProvider: 'gliner2-relex',
graphConfidence: 0.60,
providerModels: {},
};
Expand Down
14 changes: 13 additions & 1 deletion ai/executor/SelfCorrectionEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,19 @@ class SelfCorrectionEngine {
}

// 5. Contradictory Missing Note Disclaimer Correction
const hasEvidence = Boolean(options.retrievedEvidence || options.evidenceContext);
const isNegativeEvidence = (val) => {
if (!val) return false;
const str = typeof val === 'string' ? val : (typeof val === 'object' ? JSON.stringify(val) : '');
const lower = str.toLowerCase();
return lower.includes('no knowledge graph connections found') ||
lower.includes('no notes found') ||
lower.includes('no matching notes') ||
lower.includes('requested capability is not available');
};

const rawEvidence = options.retrievedEvidence || options.evidenceContext;
const hasEvidence = Boolean(rawEvidence) && !isNegativeEvidence(rawEvidence);

if (hasEvidence) {
const missingDisclaimerRegex = /(?:unfortunately,\s*)?i\s+searched\s+your\s+workspace\s+notes,\s+but\s+i\s+couldn['’]t\s+find\s+any\s+note\s+mentioning\s+[^.\n\r]+[.!]?/gi;
if (missingDisclaimerRegex.test(currentText)) {
Expand Down
125 changes: 125 additions & 0 deletions ai/graph/CommunityDetector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* CommunityDetector - Pure JS Label Propagation algorithm for Graph Community Detection
*/

const { createLogger } = require('../core/logger');

const log = createLogger('CommunityDetector');

class CommunityDetector {
/**
* Run label propagation over graph DB and update community assignments
*/
detect(graphDb, logDb = null) {
if (!graphDb?.db) return { communityCount: 0, totalNodes: 0 };
const db = graphDb.db;

try {
const entities = db.prepare("SELECT id FROM entities WHERE is_retired IS NULL OR is_retired = 0").all();
if (!entities.length) return { communityCount: 0, totalNodes: 0 };

const relationships = db.prepare("SELECT source_id, target_id FROM relationships").all();

// Build adjacency list
const adj = new Map();
entities.forEach(e => adj.set(e.id, []));

relationships.forEach(r => {
if (adj.has(r.source_id) && adj.has(r.target_id)) {
adj.get(r.source_id).push(r.target_id);
adj.get(r.target_id).push(r.source_id);
}
});

// Initialize label per node = numeric index
const labels = new Map();
entities.forEach((e, idx) => labels.set(e.id, idx + 1));

const nodeIds = entities.map(e => e.id);
const maxRounds = 30;

for (let round = 0; round < maxRounds; round++) {
let changed = 0;

// Shuffle node order for unbiased propagation
for (let i = nodeIds.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[nodeIds[i], nodeIds[j]] = [nodeIds[j], nodeIds[i]];
}

for (const nodeId of nodeIds) {
const neighbors = adj.get(nodeId) || [];
if (neighbors.length === 0) continue;

// Count neighbor label frequencies
const freq = new Map();
for (const n of neighbors) {
const l = labels.get(n);
freq.set(l, (freq.get(l) || 0) + 1);
}

// Pick max frequent label
let bestLabel = labels.get(nodeId);
let maxCount = -1;

for (const [l, count] of freq.entries()) {
if (count > maxCount) {
maxCount = count;
bestLabel = l;
}
}

if (bestLabel !== labels.get(nodeId)) {
labels.set(nodeId, bestLabel);
changed++;
}
}

if (changed === 0) break;
}

// Group entities by community label
const communitiesMap = new Map();
labels.forEach((communityId, entityId) => {
if (!communitiesMap.has(communityId)) communitiesMap.set(communityId, []);
communitiesMap.get(communityId).push(entityId);
});

// Update database tables inside a transaction
db.exec('BEGIN');
try {
db.exec('DELETE FROM communities;');
const insertCommStmt = db.prepare('INSERT INTO communities (id, label, node_count, updated_at) VALUES (?, ?, ?, datetime("now"))');
const updateEntStmt = db.prepare('UPDATE entities SET community_id = ? WHERE id = ?');

let cIndex = 1;
communitiesMap.forEach((members, labelId) => {

Check warning on line 96 in ai/graph/CommunityDetector.js

View workflow job for this annotation

GitHub Actions / build-and-test

'labelId' is defined but never used. Allowed unused args must match /^_/u
const commLabel = `Community ${cIndex}`;
insertCommStmt.run(cIndex, commLabel, members.length);
for (const entId of members) {
updateEntStmt.run(cIndex, entId);
}
cIndex++;
});

db.exec('COMMIT');
} catch (err) {
try { db.exec('ROLLBACK'); } catch { /* ignore */ }
throw err;
}

const communityCount = communitiesMap.size;
log.info(`CommunityDetector finished: identified ${communityCount} communities across ${entities.length} nodes.`);
if (logDb) {
logDb.addLog('graph', `Community detection pass complete (${communityCount} communities)`, 'info', { communityCount, totalNodes: entities.length });
}

return { communityCount, totalNodes: entities.length };
} catch (err) {
log.error('Failed community detection pass:', err.message);
return { communityCount: 0, totalNodes: 0 };
}
}
}

module.exports = CommunityDetector;
19 changes: 19 additions & 0 deletions ai/graph/EntityResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ class EntityResolver {
};
}

// Reuse existing canonical entity ID if present in database to prevent type fragmentation
if (this.graphDb?.db) {
try {
const existing = this.graphDb.db.prepare(
'SELECT id, name, canonical_name, type FROM entities WHERE LOWER(name) = LOWER(?) OR LOWER(canonical_name) = LOWER(?) LIMIT 1'
).get(clean, clean);
if (existing) {
const resolvedType = (existing.type && existing.type !== 'Concept') ? existing.type : type;
return {
id: existing.id,
name: existing.name || clean,
canonical_name: existing.canonical_name || clean,
type: resolvedType,
isAlias: true
};
}
} catch { /* ignore DB lookup error */ }
}

const defaultId = this.generateEntityId(clean, type);
return {
id: defaultId,
Expand Down
Loading
Loading