feat: harden intelligence runtime and llm providers
This commit is contained in:
112
lib/intelligence-store.mjs
Normal file
112
lib/intelligence-store.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
// Phase-1 intelligence memory. Uses node:sqlite when available and degrades to no-op.
|
||||
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
export class IntelligenceStore {
|
||||
constructor(dbPath) {
|
||||
this.dbPath = dbPath;
|
||||
this.db = null;
|
||||
this.available = false;
|
||||
this.reason = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const dir = dirname(this.dbPath);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
try {
|
||||
const sqlite = await import('node:sqlite');
|
||||
const DatabaseSync = sqlite.DatabaseSync;
|
||||
this.db = new DatabaseSync(this.dbPath);
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
sources_ok INTEGER DEFAULT 0,
|
||||
sources_degraded INTEGER DEFAULT 0,
|
||||
sources_failed INTEGER DEFAULT 0,
|
||||
direction TEXT,
|
||||
summary_json TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS predictions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT,
|
||||
confidence TEXT,
|
||||
source TEXT,
|
||||
payload_json TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
first_seen TEXT NOT NULL,
|
||||
last_seen TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
count INTEGER DEFAULT 1,
|
||||
UNIQUE(name, kind)
|
||||
);
|
||||
`);
|
||||
this.available = true;
|
||||
} catch (err) {
|
||||
this.available = false;
|
||||
this.reason = err.message;
|
||||
if (!existsSync(this.dbPath)) {
|
||||
writeFileSync(this.dbPath, '');
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
recordRun(data, delta) {
|
||||
if (!this.available || !this.db) return;
|
||||
const meta = data.meta || {};
|
||||
const timestamp = meta.timestamp || new Date().toISOString();
|
||||
this.db.prepare(`INSERT INTO runs (timestamp, sources_ok, sources_degraded, sources_failed, direction, summary_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
||||
timestamp,
|
||||
meta.sourcesOk || 0,
|
||||
meta.sourcesDegraded || 0,
|
||||
meta.sourcesFailed || 0,
|
||||
delta?.summary?.direction || null,
|
||||
JSON.stringify({ meta, delta: delta?.summary || null }),
|
||||
);
|
||||
for (const idea of data.ideas || []) {
|
||||
this.db.prepare(`INSERT INTO predictions (created_at, title, type, confidence, source, payload_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
||||
timestamp,
|
||||
idea.title || 'Untitled idea',
|
||||
idea.type || null,
|
||||
idea.confidence || null,
|
||||
idea.source || data.ideasSource || null,
|
||||
JSON.stringify(idea),
|
||||
);
|
||||
}
|
||||
this._recordEntities(data, timestamp);
|
||||
}
|
||||
|
||||
status() {
|
||||
return { available: this.available, path: this.dbPath, reason: this.reason };
|
||||
}
|
||||
|
||||
_recordEntities(data, timestamp) {
|
||||
const names = [];
|
||||
for (const item of data.acled?.deadliestEvents || []) {
|
||||
if (item.country) names.push([item.country, 'country']);
|
||||
if (item.location) names.push([item.location, 'location']);
|
||||
}
|
||||
for (const item of data.news || []) {
|
||||
if (item.region) names.push([item.region, 'region']);
|
||||
}
|
||||
for (const [name, kind] of names.slice(0, 200)) {
|
||||
this.db.prepare(`INSERT INTO entities (first_seen, last_seen, name, kind, count)
|
||||
VALUES (?, ?, ?, ?, 1)
|
||||
ON CONFLICT(name, kind) DO UPDATE SET last_seen=excluded.last_seen, count=count+1`).run(
|
||||
timestamp,
|
||||
timestamp,
|
||||
String(name).slice(0, 160),
|
||||
kind,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user