|
@@ -1,124 +1,104 @@
|
|
-const pull = require('../server/node_modules/pull-stream');
|
|
|
|
-const gui = require('../client/gui.js');
|
|
|
|
-const { getConfig } = require('../configs/config-manager.js');
|
|
|
|
-const path = require('path');
|
|
|
|
-
|
|
|
|
-const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
|
|
|
-const cooler = gui({ offline: false });
|
|
|
|
|
|
+const pull = require('../server/node_modules/pull-stream')
|
|
|
|
+const { getConfig } = require('../configs/config-manager.js')
|
|
|
|
+
|
|
|
|
+const logLimit = getConfig().ssbLogStream?.limit || 1000
|
|
|
|
+
|
|
|
|
+let cooler = null
|
|
|
|
+let ssb = null
|
|
|
|
+let opening = null
|
|
|
|
+
|
|
|
|
+function getCooler() {
|
|
|
|
+ let ssbPath = null
|
|
|
|
+ try { ssbPath = require.resolve('../server/SSB_server.js') } catch {}
|
|
|
|
+ if (ssbPath && require.cache[ssbPath]) {
|
|
|
|
+ if (!cooler) {
|
|
|
|
+ const gui = require('../client/gui.js')
|
|
|
|
+ cooler = gui({ offline: false })
|
|
|
|
+ }
|
|
|
|
+ return cooler
|
|
|
|
+ }
|
|
|
|
+ return null
|
|
|
|
+}
|
|
|
|
|
|
-const searchableTypes = [
|
|
|
|
- 'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed',
|
|
|
|
- 'votes', 'vote', 'report', 'task', 'event', 'bookmark', 'document',
|
|
|
|
- 'image', 'audio', 'video', 'market', 'forum', 'job', 'project',
|
|
|
|
- 'contact', 'pub', 'pixelia', 'bankWallet', 'bankClaim', 'aiExchange'
|
|
|
|
-];
|
|
|
|
|
|
+async function openSsb() {
|
|
|
|
+ const c = getCooler()
|
|
|
|
+ if (!c) return null
|
|
|
|
+ if (ssb && ssb.closed === false) return ssb
|
|
|
|
+ if (!opening) opening = c.open().then(x => (ssb = x)).finally(() => { opening = null })
|
|
|
|
+ await opening
|
|
|
|
+ return ssb
|
|
|
|
+}
|
|
|
|
|
|
-const clip = (s, n) => String(s || '').slice(0, n);
|
|
|
|
-const squash = s => String(s || '').replace(/\s+/g, ' ').trim();
|
|
|
|
-const compact = s => squash(clip(s, 160));
|
|
|
|
|
|
+const clip = (s, n) => String(s || '').slice(0, n)
|
|
|
|
+const squash = s => String(s || '').replace(/\s+/g, ' ').trim()
|
|
|
|
+const compact = s => squash(clip(s, 160))
|
|
|
|
+const normalize = s => String(s || '').toLowerCase().replace(/\s+/g, ' ').replace(/[^\p{L}\p{N}\s]+/gu, '').trim()
|
|
|
|
|
|
function fieldsForSnippet(type, c) {
|
|
function fieldsForSnippet(type, c) {
|
|
- switch (type) {
|
|
|
|
- case 'aiExchange': return [c?.question, clip(squash(c?.answer || ''), 120)];
|
|
|
|
- case 'post': return [c?.text, ...(c?.tags || [])];
|
|
|
|
- case 'about': return [c?.about, c?.name, c?.description];
|
|
|
|
- case 'curriculum': return [c?.name, c?.description, c?.location];
|
|
|
|
- case 'tribe': return [c?.title, c?.description, ...(c?.tags || [])];
|
|
|
|
- case 'transfer': return [c?.from, c?.to, String(c?.amount), c?.status];
|
|
|
|
- case 'feed': return [c?.text, ...(c?.tags || [])];
|
|
|
|
- case 'votes': return [c?.question, c?.status];
|
|
|
|
- case 'vote': return [c?.vote?.link, String(c?.vote?.value)];
|
|
|
|
- case 'report': return [c?.title, c?.severity, c?.status];
|
|
|
|
- case 'task': return [c?.title, c?.status];
|
|
|
|
- case 'event': return [c?.title, c?.date, c?.location];
|
|
|
|
- case 'bookmark': return [c?.url, c?.description];
|
|
|
|
- case 'document': return [c?.title, c?.description];
|
|
|
|
- case 'image': return [c?.title, c?.description];
|
|
|
|
- case 'audio': return [c?.title, c?.description];
|
|
|
|
- case 'video': return [c?.title, c?.description];
|
|
|
|
- case 'market': return [c?.title, String(c?.price), c?.status];
|
|
|
|
- case 'forum': return [c?.title, c?.category, c?.text];
|
|
|
|
- case 'job': return [c?.title, c?.job_type, String(c?.salary), c?.status];
|
|
|
|
- case 'project': return [c?.title, c?.status, String(c?.progress)];
|
|
|
|
- case 'contact': return [c?.contact];
|
|
|
|
- case 'pub': return [c?.address?.key, c?.address?.host];
|
|
|
|
- case 'pixelia': return [c?.author];
|
|
|
|
- case 'bankWallet': return [c?.address];
|
|
|
|
- case 'bankClaim': return [String(c?.amount), c?.epochId, c?.txid];
|
|
|
|
- default: return [];
|
|
|
|
- }
|
|
|
|
|
|
+ if (type === 'aiExchange') return [c?.question, clip(squash(c?.answer || ''), 120)]
|
|
|
|
+ return []
|
|
}
|
|
}
|
|
|
|
|
|
async function publishExchange({ q, a, ctx = [], tokens = {} }) {
|
|
async function publishExchange({ q, a, ctx = [], tokens = {} }) {
|
|
- const ssbClient = await cooler.open();
|
|
|
|
-
|
|
|
|
|
|
+ const s = await openSsb()
|
|
|
|
+ if (!s) return null
|
|
const content = {
|
|
const content = {
|
|
type: 'aiExchange',
|
|
type: 'aiExchange',
|
|
question: clip(String(q || ''), 2000),
|
|
question: clip(String(q || ''), 2000),
|
|
answer: clip(String(a || ''), 5000),
|
|
answer: clip(String(a || ''), 5000),
|
|
- ctx: ctx.slice(0, 12).map(s => clip(String(s || ''), 800)),
|
|
|
|
|
|
+ ctx: ctx.slice(0, 12).map(x => clip(String(x || ''), 800)),
|
|
timestamp: Date.now()
|
|
timestamp: Date.now()
|
|
- };
|
|
|
|
-
|
|
|
|
|
|
+ }
|
|
return new Promise((resolve, reject) => {
|
|
return new Promise((resolve, reject) => {
|
|
- ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res));
|
|
|
|
- });
|
|
|
|
|
|
+ s.publish(content, (err, res) => err ? reject(err) : resolve(res))
|
|
|
|
+ })
|
|
}
|
|
}
|
|
|
|
|
|
async function buildContext(maxItems = 100) {
|
|
async function buildContext(maxItems = 100) {
|
|
- const ssb = await cooler.open();
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
|
+ const s = await openSsb()
|
|
|
|
+ if (!s) return ''
|
|
|
|
+ return new Promise((resolve) => {
|
|
pull(
|
|
pull(
|
|
- ssb.createLogStream({ reverse: true, limit: logLimit }),
|
|
|
|
|
|
+ s.createLogStream({ reverse: true, limit: logLimit }),
|
|
pull.collect((err, msgs) => {
|
|
pull.collect((err, msgs) => {
|
|
- if (err) return reject(err);
|
|
|
|
-
|
|
|
|
- const tombstoned = new Set();
|
|
|
|
- const latest = new Map();
|
|
|
|
-
|
|
|
|
|
|
+ if (err || !Array.isArray(msgs)) return resolve('')
|
|
|
|
+ const lines = []
|
|
for (const { value } of msgs) {
|
|
for (const { value } of msgs) {
|
|
- const c = value?.content;
|
|
|
|
- if (c?.type === 'tombstone' && c?.target) tombstoned.add(c.target);
|
|
|
|
|
|
+ const c = value && value.content || {}
|
|
|
|
+ if (c.type !== 'aiExchange') continue
|
|
|
|
+ const d = new Date(value.timestamp || 0).toISOString().slice(0, 10)
|
|
|
|
+ const q = compact(c.question)
|
|
|
|
+ const a = compact(c.answer)
|
|
|
|
+ lines.push(`[${d}] (AIExchange) Q: ${q} | A: ${a}`)
|
|
|
|
+ if (lines.length >= maxItems) break
|
|
}
|
|
}
|
|
|
|
+ if (lines.length === 0) return resolve('')
|
|
|
|
+ resolve(`## AIEXCHANGE\n\n${lines.join('\n')}`)
|
|
|
|
+ })
|
|
|
|
+ )
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
|
|
- for (const { key, value } of msgs) {
|
|
|
|
- const author = value?.author;
|
|
|
|
- const content = value?.content || {};
|
|
|
|
- const type = content?.type;
|
|
|
|
- const ts = value?.timestamp || 0;
|
|
|
|
-
|
|
|
|
- if (!searchableTypes.includes(type) || tombstoned.has(key)) continue;
|
|
|
|
-
|
|
|
|
- const uniqueKey = type === 'about' ? content.about : key;
|
|
|
|
- if (!latest.has(uniqueKey) || (latest.get(uniqueKey)?.value?.timestamp || 0) < ts) {
|
|
|
|
- latest.set(uniqueKey, { key, value });
|
|
|
|
|
|
+async function getBestTrainedAnswer(question) {
|
|
|
|
+ const s = await openSsb()
|
|
|
|
+ if (!s) return null
|
|
|
|
+ const want = normalize(question)
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
+ pull(
|
|
|
|
+ s.createLogStream({ reverse: true, limit: logLimit }),
|
|
|
|
+ pull.collect((err, msgs) => {
|
|
|
|
+ if (err || !Array.isArray(msgs)) return resolve(null)
|
|
|
|
+ for (const { value } of msgs) {
|
|
|
|
+ const c = value && value.content || {}
|
|
|
|
+ if (c.type !== 'aiExchange') continue
|
|
|
|
+ if (normalize(c.question) === want) {
|
|
|
|
+ return resolve({ answer: String(c.answer || '').trim(), ctx: Array.isArray(c.ctx) ? c.ctx : [] })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
- const grouped = {};
|
|
|
|
- Array.from(latest.values())
|
|
|
|
- .sort((a, b) => (b.value.timestamp || 0) - (a.value.timestamp || 0))
|
|
|
|
- .slice(0, maxItems)
|
|
|
|
- .forEach(({ value }) => {
|
|
|
|
- const content = value.content;
|
|
|
|
- const type = content.type;
|
|
|
|
- const fields = fieldsForSnippet(type, content).filter(Boolean).map(compact).filter(Boolean).join(' | ');
|
|
|
|
- if (!fields) return;
|
|
|
|
- const date = new Date(value.timestamp || 0).toISOString().slice(0, 10);
|
|
|
|
- grouped[type] = grouped[type] || [];
|
|
|
|
- grouped[type].push(`[${date}] (${type}) ${fields}`);
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- const contextSections = Object.entries(grouped)
|
|
|
|
- .map(([type, lines]) => `## ${type.toUpperCase()}\n\n${lines.slice(0, 20).join('\n')}`)
|
|
|
|
- .join('\n\n');
|
|
|
|
-
|
|
|
|
- const finalContext = contextSections ? contextSections : '';
|
|
|
|
- resolve(finalContext);
|
|
|
|
|
|
+ resolve(null)
|
|
})
|
|
})
|
|
- );
|
|
|
|
- });
|
|
|
|
|
|
+ )
|
|
|
|
+ })
|
|
}
|
|
}
|
|
|
|
|
|
-module.exports = { fieldsForSnippet, buildContext, clip, publishExchange };
|
|
|
|
-
|
|
|
|
|
|
+module.exports = { fieldsForSnippet, buildContext, clip, publishExchange, getBestTrainedAnswer }
|