Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m41s
Details
CI/CD / deploy (push) Successful in 3m41s
Details
This commit is contained in:
commit
e3ef126eab
|
|
@ -16,5 +16,5 @@ export const auctionsModule: RSpaceModule = {
|
||||||
icon: '🏛',
|
icon: '🏛',
|
||||||
description: 'Community auctions with USDC',
|
description: 'Community auctions with USDC',
|
||||||
routes,
|
routes,
|
||||||
scoping: { scope: 'space', default: 'disabled' },
|
scoping: { defaultScope: 'space', userConfigurable: true },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,12 @@ export function registerAgentsTools(server: McpServer, syncServer: SyncServer) {
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const docId = agentChannelDocId(space, channel_id);
|
const docId = agentChannelDocId(space, channel_id);
|
||||||
const authorName = access.username || "Agent";
|
const authorName = access.claims?.username || "Agent";
|
||||||
|
|
||||||
syncServer.changeDoc<AgentChannelDoc>(docId, `agent post ${id}`, (d) => {
|
syncServer.changeDoc<AgentChannelDoc>(docId, `agent post ${id}`, (d) => {
|
||||||
if (!d.posts) (d as any).posts = {};
|
if (!d.posts) (d as any).posts = {};
|
||||||
d.posts[id] = {
|
d.posts[id] = {
|
||||||
id, channelId: channel_id, authorAgentId: access.did || '', authorName,
|
id, channelId: channel_id, authorAgentId: access.claims?.sub || '', authorName,
|
||||||
content, payload: payload || null, replyTo: null, votes: {},
|
content, payload: payload || null, replyTo: null, votes: {},
|
||||||
createdAt: Date.now(), updatedAt: Date.now(),
|
createdAt: Date.now(), updatedAt: Date.now(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
@ -149,11 +149,11 @@ export function registerAgentsTools(server: McpServer, syncServer: SyncServer) {
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const docId = agentChannelDocId(space, channel_id);
|
const docId = agentChannelDocId(space, channel_id);
|
||||||
const authorName = access.username || "Agent";
|
const authorName = access.claims?.username || "Agent";
|
||||||
|
|
||||||
syncServer.changeDoc<AgentChannelDoc>(docId, `reply to ${post_id}`, (d) => {
|
syncServer.changeDoc<AgentChannelDoc>(docId, `reply to ${post_id}`, (d) => {
|
||||||
d.posts[id] = {
|
d.posts[id] = {
|
||||||
id, channelId: channel_id, authorAgentId: access.did || '', authorName,
|
id, channelId: channel_id, authorAgentId: access.claims?.sub || '', authorName,
|
||||||
content, payload: payload || null, replyTo: post_id, votes: {},
|
content, payload: payload || null, replyTo: post_id, votes: {},
|
||||||
createdAt: Date.now(), updatedAt: Date.now(),
|
createdAt: Date.now(), updatedAt: Date.now(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
@ -182,7 +182,7 @@ export function registerAgentsTools(server: McpServer, syncServer: SyncServer) {
|
||||||
return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Post not found" }) }] };
|
return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Post not found" }) }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const voterId = access.did || "anon";
|
const voterId = access.claims?.sub || "anon";
|
||||||
const docId = agentChannelDocId(space, channel_id);
|
const docId = agentChannelDocId(space, channel_id);
|
||||||
|
|
||||||
syncServer.changeDoc<AgentChannelDoc>(docId, `vote on ${post_id}`, (d) => {
|
syncServer.changeDoc<AgentChannelDoc>(docId, `vote on ${post_id}`, (d) => {
|
||||||
|
|
|
||||||
|
|
@ -18,42 +18,14 @@ import { verifyToken, extractToken } from "./auth";
|
||||||
import type { EncryptIDClaims } from "./auth";
|
import type { EncryptIDClaims } from "./auth";
|
||||||
import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes";
|
import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes";
|
||||||
import type { MiAction } from "../lib/mi-actions";
|
import type { MiAction } from "../lib/mi-actions";
|
||||||
|
import { spaceKnowledgeIndex } from "./space-knowledge";
|
||||||
|
import { spaceMemory, streamWithMemoryCapture } from "./space-memory";
|
||||||
|
import { runAgenticLoop } from "./mi-agent";
|
||||||
|
// Module imports retained for /suggestions endpoint
|
||||||
import { getUpcomingEventsForMI } from "../modules/rcal/mod";
|
import { getUpcomingEventsForMI } from "../modules/rcal/mod";
|
||||||
import { getRecentVaultNotesForMI } from "../modules/rnotes/mod";
|
import { getRecentVaultNotesForMI } from "../modules/rnotes/mod";
|
||||||
import { getRecentTasksForMI } from "../modules/rtasks/mod";
|
import { getRecentTasksForMI } from "../modules/rtasks/mod";
|
||||||
import { getRecentCampaignsForMI } from "../modules/rsocials/mod";
|
|
||||||
import { getRecentContactsForMI } from "../modules/rnetwork/mod";
|
|
||||||
import { getRecentThreadsForMI } from "../modules/rinbox/mod";
|
|
||||||
import { getRecentCommitmentsForMI } from "../modules/rtime/mod";
|
|
||||||
import { getRecentFilesForMI } from "../modules/rfiles/mod";
|
|
||||||
import { getUpcomingRemindersForMI } from "../modules/rschedule/mod";
|
|
||||||
import { getMapPinsForMI } from "../modules/rmaps/mod";
|
|
||||||
import { getRecentMeetingsForMI } from "../modules/rmeets/mod";
|
|
||||||
import { getRecentVideosForMI } from "../modules/rtube/mod";
|
|
||||||
import { getRecentMessagesForMI } from "../modules/rchats/mod";
|
|
||||||
import { getRecentPublicationsForMI } from "../modules/rpubs/mod";
|
|
||||||
import { getRecentDesignsForMI } from "../modules/rswag/mod";
|
|
||||||
import { getRecentSheetsForMI } from "../modules/rsheets/mod";
|
|
||||||
import { getRecentDocsForMI } from "../modules/rdocs/mod";
|
import { getRecentDocsForMI } from "../modules/rdocs/mod";
|
||||||
import { getRecentSessionsForMI } from "../modules/rdesign/mod";
|
|
||||||
import { getSharedAlbumsForMI } from "../modules/rphotos/mod";
|
|
||||||
import { getRecentFlowsForMI } from "../modules/rflows/mod";
|
|
||||||
import { getRecentIntentsForMI } from "../modules/rexchange/mod";
|
|
||||||
import { getRecentOrdersForMI } from "../modules/rcart/mod";
|
|
||||||
import { getActiveProposalsForMI } from "../modules/rvote/mod";
|
|
||||||
import { getRecentBooksForMI } from "../modules/rbooks/mod";
|
|
||||||
import { getRecentSplatsForMI } from "../modules/rsplat/mod";
|
|
||||||
import { getRecentTripsForMI } from "../modules/rtrips/mod";
|
|
||||||
import { getActiveListingsForMI } from "../modules/rbnb/mod";
|
|
||||||
import { getActiveVehiclesForMI } from "../modules/rvnb/mod";
|
|
||||||
import { getForumInstancesForMI } from "../modules/rforum/mod";
|
|
||||||
import { getRecentChoiceSessionsForMI } from "../modules/rchoices/mod";
|
|
||||||
import { getActivePromptsForMI } from "../modules/crowdsurf/mod";
|
|
||||||
import { getGovShapesForMI } from "../modules/rgov/mod";
|
|
||||||
import { getCrdtTokensForMI } from "../modules/rwallet/mod";
|
|
||||||
import { getCanvasSummaryForMI } from "../modules/rspace/mod";
|
|
||||||
import { getDataSummaryForMI } from "../modules/rdata/mod";
|
|
||||||
import { runAgenticLoop } from "./mi-agent";
|
|
||||||
import { generateImage, generateVideoViaFal } from "./mi-media";
|
import { generateImage, generateVideoViaFal } from "./mi-media";
|
||||||
import { queryModuleContent } from "./mi-data-queries";
|
import { queryModuleContent } from "./mi-data-queries";
|
||||||
|
|
||||||
|
|
@ -186,271 +158,12 @@ mi.post("/ask", async (c) => {
|
||||||
const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||||
const timeContext = `${weekdays[now.getUTCDay()]}, ${now.toISOString().split("T")[0]} at ${now.toISOString().split("T")[1].split(".")[0]} UTC`;
|
const timeContext = `${weekdays[now.getUTCDay()]}, ${now.toISOString().split("T")[0]} at ${now.toISOString().split("T")[1].split(".")[0]} UTC`;
|
||||||
|
|
||||||
let calendarContext = "";
|
// ── Build ranked knowledge context + conversation memory ──
|
||||||
let notesContext = "";
|
let rankedKnowledgeContext = "";
|
||||||
let tasksContext = "";
|
let conversationMemoryContext = "";
|
||||||
let campaignsContext = "";
|
|
||||||
let contactsContext = "";
|
|
||||||
let inboxContext = "";
|
|
||||||
let commitmentsContext = "";
|
|
||||||
let filesContext = "";
|
|
||||||
let remindersContext = "";
|
|
||||||
let mapsContext = "";
|
|
||||||
let meetsContext = "";
|
|
||||||
let tubeContext = "";
|
|
||||||
let chatsContext = "";
|
|
||||||
let pubsContext = "";
|
|
||||||
let swagContext = "";
|
|
||||||
let sheetsContext = "";
|
|
||||||
let docsContext = "";
|
|
||||||
let designContext = "";
|
|
||||||
let photosContext = "";
|
|
||||||
let flowsContext = "";
|
|
||||||
let exchangeContext = "";
|
|
||||||
let cartContext = "";
|
|
||||||
let voteContext = "";
|
|
||||||
let booksContext = "";
|
|
||||||
let splatsContext = "";
|
|
||||||
let tripsContext = "";
|
|
||||||
let bnbContext = "";
|
|
||||||
let vnbContext = "";
|
|
||||||
let forumContext = "";
|
|
||||||
let choicesContext = "";
|
|
||||||
let crowdsurfContext = "";
|
|
||||||
let govContext = "";
|
|
||||||
let walletContext = "";
|
|
||||||
let canvasContext = "";
|
|
||||||
if (space) {
|
if (space) {
|
||||||
const upcoming = getUpcomingEventsForMI(space);
|
rankedKnowledgeContext = spaceKnowledgeIndex.getRankedContext(space, query, 18);
|
||||||
if (upcoming.length > 0) {
|
conversationMemoryContext = await spaceMemory.getRelevantTurns(space, query, 3);
|
||||||
const lines = upcoming.map((e) => {
|
|
||||||
const date = e.allDay ? e.start.split("T")[0] : e.start;
|
|
||||||
let line = `- ${date}: ${e.title}`;
|
|
||||||
if (e.location) line += ` (${e.location})`;
|
|
||||||
else if (e.isVirtual) line += ` (virtual)`;
|
|
||||||
if (e.tags?.length) line += ` [${e.tags.join(", ")}]`;
|
|
||||||
return line;
|
|
||||||
});
|
|
||||||
calendarContext = `\n- Upcoming events (next 14 days):\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaultNotes = getRecentVaultNotesForMI(space, 3);
|
|
||||||
if (vaultNotes.length > 0) {
|
|
||||||
const lines = vaultNotes.map((n) =>
|
|
||||||
`- "${n.title}" in ${n.vaultName} (${n.path})${n.tags.length ? ` [${n.tags.join(", ")}]` : ""}`
|
|
||||||
);
|
|
||||||
notesContext = `\n- Recent vault notes:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTasks = getRecentTasksForMI(space, 5);
|
|
||||||
if (openTasks.length > 0) {
|
|
||||||
const lines = openTasks.map((t) =>
|
|
||||||
`- "${t.title}" [${t.status}]${t.priority ? ` (${t.priority})` : ""}${t.description ? `: ${t.description.slice(0, 80)}` : ""}`
|
|
||||||
);
|
|
||||||
tasksContext = `\n- Open tasks:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentCampaigns = getRecentCampaignsForMI(space, 3);
|
|
||||||
if (recentCampaigns.length > 0) {
|
|
||||||
const lines = recentCampaigns.map((c) =>
|
|
||||||
`- "${c.title}" (${c.platforms.join(", ")}, ${c.postCount} posts)`
|
|
||||||
);
|
|
||||||
campaignsContext = `\n- Recent campaigns:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contacts = getRecentContactsForMI(space, 3);
|
|
||||||
if (contacts.length > 0) {
|
|
||||||
const lines = contacts.map((c) =>
|
|
||||||
`- ${c.name} (${c.role})${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`
|
|
||||||
);
|
|
||||||
contactsContext = `\n- Network contacts:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentThreads = getRecentThreadsForMI(space, 3);
|
|
||||||
if (recentThreads.length > 0) {
|
|
||||||
const lines = recentThreads.map((t) =>
|
|
||||||
`- "${t.subject}" from ${t.fromAddress || "unknown"} [${t.status}]${t.isRead ? "" : " (unread)"}`
|
|
||||||
);
|
|
||||||
inboxContext = `\n- Recent emails:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitments = getRecentCommitmentsForMI(space, 3);
|
|
||||||
if (commitments.length > 0) {
|
|
||||||
const lines = commitments.map((c) =>
|
|
||||||
`- ${c.memberName}: ${c.hours}h ${c.skill} — ${c.desc.slice(0, 80)}`
|
|
||||||
);
|
|
||||||
commitmentsContext = `\n- Active commitments:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentFiles = getRecentFilesForMI(space, 3);
|
|
||||||
if (recentFiles.length > 0) {
|
|
||||||
const lines = recentFiles.map((f) =>
|
|
||||||
`- ${f.title || f.originalFilename} (${f.mimeType || "unknown"})`
|
|
||||||
);
|
|
||||||
filesContext = `\n- Recent files:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reminders = getUpcomingRemindersForMI(space, 14, 3);
|
|
||||||
if (reminders.length > 0) {
|
|
||||||
const lines = reminders.map((r) => {
|
|
||||||
const date = new Date(r.remindAt).toISOString().split("T")[0];
|
|
||||||
return `- ${date}: ${r.title}${r.sourceModule ? ` (from ${r.sourceModule})` : ""}`;
|
|
||||||
});
|
|
||||||
remindersContext = `\n- Upcoming reminders:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pins = getMapPinsForMI(space, 5);
|
|
||||||
if (pins.length > 0) {
|
|
||||||
const lines = pins.map((p) => `- "${p.label}" (${p.type}) at ${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}`);
|
|
||||||
mapsContext = `\n- Map pins:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const meetings = getRecentMeetingsForMI(space, 3);
|
|
||||||
if (meetings.length > 0) {
|
|
||||||
const lines = meetings.map((m) => `- "${m.title}" (${m.participantCount} participants)`);
|
|
||||||
meetsContext = `\n- Recent meetings:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playlists = getRecentVideosForMI(space, 3);
|
|
||||||
if (playlists.length > 0) {
|
|
||||||
const lines = playlists.map((v) => `- "${v.name}" (${v.entryCount} entries)`);
|
|
||||||
tubeContext = `\n- Playlists:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatMsgs = getRecentMessagesForMI(space, 3);
|
|
||||||
if (chatMsgs.length > 0) {
|
|
||||||
const lines = chatMsgs.map((m) => `- [${m.channel}] ${m.author}: ${m.content.slice(0, 80)}`);
|
|
||||||
chatsContext = `\n- Recent chats:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pubs = getRecentPublicationsForMI(space, 3);
|
|
||||||
if (pubs.length > 0) {
|
|
||||||
const lines = pubs.map((p) => `- "${p.title}" by ${p.author} (${p.format})`);
|
|
||||||
pubsContext = `\n- Publications:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const swagDesigns = getRecentDesignsForMI(space, 3);
|
|
||||||
if (swagDesigns.length > 0) {
|
|
||||||
const lines = swagDesigns.map((d) => `- "${d.title}" (${d.productType}, ${d.status})`);
|
|
||||||
swagContext = `\n- Store designs:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sheets = getRecentSheetsForMI(space, 3);
|
|
||||||
if (sheets.length > 0) {
|
|
||||||
const lines = sheets.map((s) => `- "${s.name}" (${s.cellCount} cells)`);
|
|
||||||
sheetsContext = `\n- Spreadsheets:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentDocs = getRecentDocsForMI(space, 3);
|
|
||||||
if (recentDocs.length > 0) {
|
|
||||||
const lines = recentDocs.map((n) =>
|
|
||||||
`- "${n.title}" (${n.type}${n.tags.length ? `, tags: ${n.tags.join(", ")}` : ""}): ${n.contentPlain.slice(0, 100)}…`
|
|
||||||
);
|
|
||||||
docsContext = `\n- Recent docs:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const designSessions = getRecentSessionsForMI(space, 3);
|
|
||||||
if (designSessions.length > 0) {
|
|
||||||
const lines = designSessions.map((s) => `- "${s.title}" (${s.pageCount} pages, ${s.frameCount} frames)`);
|
|
||||||
designContext = `\n- Design sessions:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const albums = getSharedAlbumsForMI(space, 3);
|
|
||||||
if (albums.length > 0) {
|
|
||||||
const lines = albums.map((a) => `- "${a.name}"`);
|
|
||||||
photosContext = `\n- Shared albums:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const flows = getRecentFlowsForMI(space, 3);
|
|
||||||
if (flows.length > 0) {
|
|
||||||
const lines = flows.map((f) => `- "${f.name}" (${f.nodeCount} nodes)`);
|
|
||||||
flowsContext = `\n- Flows:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const intents = getRecentIntentsForMI(space, 3);
|
|
||||||
if (intents.length > 0) {
|
|
||||||
const lines = intents.map((i) => `- ${i.side} ${i.tokenId} [${i.status}]`);
|
|
||||||
exchangeContext = `\n- Exchange intents:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orders = getRecentOrdersForMI(space, 3);
|
|
||||||
if (orders.length > 0) {
|
|
||||||
const lines = orders.map((o) => `- "${o.title}" [${o.status}]`);
|
|
||||||
cartContext = `\n- Recent orders:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const proposals = getActiveProposalsForMI(space, 3);
|
|
||||||
if (proposals.length > 0) {
|
|
||||||
const lines = proposals.map((p) => `- "${p.title}" [${p.status}] (${p.voteCount} votes)`);
|
|
||||||
voteContext = `\n- Proposals:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const books = getRecentBooksForMI(space, 3);
|
|
||||||
if (books.length > 0) {
|
|
||||||
const lines = books.map((b) => `- "${b.title}" by ${b.author}`);
|
|
||||||
booksContext = `\n- Books:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splats = getRecentSplatsForMI(space, 3);
|
|
||||||
if (splats.length > 0) {
|
|
||||||
const lines = splats.map((s) => `- "${s.title}" (${s.format})`);
|
|
||||||
splatsContext = `\n- 3D scenes:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trips = getRecentTripsForMI(space, 3);
|
|
||||||
if (trips.length > 0) {
|
|
||||||
const lines = trips.map((t) => `- "${t.title}" [${t.status}] (${t.destinationCount} destinations)`);
|
|
||||||
tripsContext = `\n- Trips:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bnbListings = getActiveListingsForMI(space, 3);
|
|
||||||
if (bnbListings.length > 0) {
|
|
||||||
const lines = bnbListings.map((l) => `- "${l.title}" (${l.type}, ${l.economy})`);
|
|
||||||
bnbContext = `\n- BnB listings:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vnbVehicles = getActiveVehiclesForMI(space, 3);
|
|
||||||
if (vnbVehicles.length > 0) {
|
|
||||||
const lines = vnbVehicles.map((v) => `- "${v.title}" (${v.type}, ${v.economy})`);
|
|
||||||
vnbContext = `\n- Vehicles:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const forumInstances = getForumInstancesForMI(space, 3);
|
|
||||||
if (forumInstances.length > 0) {
|
|
||||||
const lines = forumInstances.map((i) => `- "${i.name}" (${i.domain || "pending"}) [${i.status}]`);
|
|
||||||
forumContext = `\n- Forum instances:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const choiceSessions = getRecentChoiceSessionsForMI(space, 3);
|
|
||||||
if (choiceSessions.length > 0) {
|
|
||||||
const lines = choiceSessions.map((s) => `- "${s.title}" (${s.type}, ${s.optionCount} options)`);
|
|
||||||
choicesContext = `\n- Choice sessions:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csPrompts = getActivePromptsForMI(space, 3);
|
|
||||||
if (csPrompts.length > 0) {
|
|
||||||
const lines = csPrompts.map((p) => `- "${p.text.slice(0, 60)}" (${p.swipeCount}/${p.threshold})`);
|
|
||||||
crowdsurfContext = `\n- Crowdsurf prompts:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const govShapes = getGovShapesForMI(space);
|
|
||||||
if (govShapes.length > 0) {
|
|
||||||
const lines = govShapes.map((s) => `- ${s.type}: ${s.count}`);
|
|
||||||
govContext = `\n- Governance shapes:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const crdtTokens = getCrdtTokensForMI(space, 3);
|
|
||||||
if (crdtTokens.length > 0) {
|
|
||||||
const lines = crdtTokens.map((t) => `- ${t.symbol} (${t.name}): supply ${t.totalSupply}`);
|
|
||||||
walletContext = `\n- CRDT tokens:\n${lines.join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvasSummary = getCanvasSummaryForMI(space);
|
|
||||||
if (canvasSummary.length > 0 && canvasSummary[0].totalShapes > 0) {
|
|
||||||
const s = canvasSummary[0];
|
|
||||||
const top = s.typeBreakdown.slice(0, 5).map((t) => `${t.type}: ${t.count}`).join(", ");
|
|
||||||
canvasContext = `\n- Canvas: ${s.totalShapes} shapes (${top})`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemPrompt = `You are mi (mycelial intelligence), the intelligent assistant for rSpace — a self-hosted, community-run platform.
|
const systemPrompt = `You are mi (mycelial intelligence), the intelligent assistant for rSpace — a self-hosted, community-run platform.
|
||||||
|
|
@ -476,7 +189,7 @@ ${moduleCapabilities}
|
||||||
When the user asks to create a social media campaign, use create-content with module rsocials, contentType campaign, body.rawBrief set to the user's description, body.navigateToWizard true.
|
When the user asks to create a social media campaign, use create-content with module rsocials, contentType campaign, body.rawBrief set to the user's description, body.navigateToWizard true.
|
||||||
|
|
||||||
## Current Context
|
## Current Context
|
||||||
${contextSection}${calendarContext}${notesContext}${tasksContext}${campaignsContext}${contactsContext}${inboxContext}${commitmentsContext}${filesContext}${remindersContext}${mapsContext}${meetsContext}${tubeContext}${chatsContext}${pubsContext}${swagContext}${sheetsContext}${docsContext}${designContext}${photosContext}${flowsContext}${exchangeContext}${cartContext}${voteContext}${booksContext}${splatsContext}${tripsContext}${bnbContext}${vnbContext}${forumContext}${choicesContext}${crowdsurfContext}${govContext}${walletContext}${canvasContext}
|
${contextSection}${rankedKnowledgeContext}${conversationMemoryContext}
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
- Be concise and helpful. Keep responses short (2-4 sentences) unless the user asks for detail.
|
- Be concise and helpful. Keep responses short (2-4 sentences) unless the user asks for detail.
|
||||||
|
|
@ -596,7 +309,10 @@ Use requireConfirm:true for destructive batches.`;
|
||||||
maxTurns: 5,
|
maxTurns: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Response(body, {
|
// Wrap stream to capture response text for conversation memory
|
||||||
|
const responseStream = space ? streamWithMemoryCapture(body, space, query) : body;
|
||||||
|
|
||||||
|
return new Response(responseStream, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-ndjson",
|
"Content-Type": "application/x-ndjson",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Trigram + Jaccard similarity utilities for MI knowledge ranking.
|
||||||
|
* Pure functions, zero dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Extract character trigrams from text (lowercased). */
|
||||||
|
export function trigrams(text: string): Set<string> {
|
||||||
|
const s = text.toLowerCase().replace(/[^\w\s]/g, "");
|
||||||
|
const set = new Set<string>();
|
||||||
|
for (let i = 0; i <= s.length - 3; i++) {
|
||||||
|
set.add(s.slice(i, i + 3));
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Jaccard similarity between two trigram sets (0..1). */
|
||||||
|
export function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
|
||||||
|
if (a.size === 0 && b.size === 0) return 0;
|
||||||
|
let intersection = 0;
|
||||||
|
for (const t of a) {
|
||||||
|
if (b.has(t)) intersection++;
|
||||||
|
}
|
||||||
|
const union = a.size + b.size - intersection;
|
||||||
|
return union === 0 ? 0 : intersection / union;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,581 @@
|
||||||
|
/**
|
||||||
|
* Per-Space Knowledge Index — ranked retrieval for MI context injection.
|
||||||
|
*
|
||||||
|
* Instead of dumping all 35 module summaries into every prompt,
|
||||||
|
* this index builds KnowledgeEntry objects from each module and
|
||||||
|
* returns only the top-N most relevant entries for a given query
|
||||||
|
* using trigram similarity + recency boost + category weighting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { trigrams, jaccardSimilarity } from "./mi-trigrams";
|
||||||
|
|
||||||
|
import { getUpcomingEventsForMI } from "../modules/rcal/mod";
|
||||||
|
import { getRecentVaultNotesForMI } from "../modules/rnotes/mod";
|
||||||
|
import { getRecentTasksForMI } from "../modules/rtasks/mod";
|
||||||
|
import { getRecentCampaignsForMI } from "../modules/rsocials/mod";
|
||||||
|
import { getRecentContactsForMI } from "../modules/rnetwork/mod";
|
||||||
|
import { getRecentThreadsForMI } from "../modules/rinbox/mod";
|
||||||
|
import { getRecentCommitmentsForMI } from "../modules/rtime/mod";
|
||||||
|
import { getRecentFilesForMI } from "../modules/rfiles/mod";
|
||||||
|
import { getUpcomingRemindersForMI } from "../modules/rschedule/mod";
|
||||||
|
import { getMapPinsForMI } from "../modules/rmaps/mod";
|
||||||
|
import { getRecentMeetingsForMI } from "../modules/rmeets/mod";
|
||||||
|
import { getRecentVideosForMI } from "../modules/rtube/mod";
|
||||||
|
import { getRecentMessagesForMI } from "../modules/rchats/mod";
|
||||||
|
import { getRecentAgentPostsForMI } from "../modules/ragents/mod";
|
||||||
|
import { getRecentPublicationsForMI } from "../modules/rpubs/mod";
|
||||||
|
import { getRecentDesignsForMI } from "../modules/rswag/mod";
|
||||||
|
import { getRecentSheetsForMI } from "../modules/rsheets/mod";
|
||||||
|
import { getRecentDocsForMI } from "../modules/rdocs/mod";
|
||||||
|
import { getRecentSessionsForMI } from "../modules/rdesign/mod";
|
||||||
|
import { getSharedAlbumsForMI } from "../modules/rphotos/mod";
|
||||||
|
import { getRecentFlowsForMI } from "../modules/rflows/mod";
|
||||||
|
import { getRecentIntentsForMI } from "../modules/rexchange/mod";
|
||||||
|
import { getRecentOrdersForMI } from "../modules/rcart/mod";
|
||||||
|
import { getActiveProposalsForMI } from "../modules/rvote/mod";
|
||||||
|
import { getRecentBooksForMI } from "../modules/rbooks/mod";
|
||||||
|
import { getRecentSplatsForMI } from "../modules/rsplat/mod";
|
||||||
|
import { getRecentTripsForMI } from "../modules/rtrips/mod";
|
||||||
|
import { getActiveListingsForMI } from "../modules/rbnb/mod";
|
||||||
|
import { getActiveVehiclesForMI } from "../modules/rvnb/mod";
|
||||||
|
import { getForumInstancesForMI } from "../modules/rforum/mod";
|
||||||
|
import { getRecentChoiceSessionsForMI } from "../modules/rchoices/mod";
|
||||||
|
import { getActivePromptsForMI } from "../modules/crowdsurf/mod";
|
||||||
|
import { getGovShapesForMI } from "../modules/rgov/mod";
|
||||||
|
import { getCrdtTokensForMI } from "../modules/rwallet/mod";
|
||||||
|
import { getCanvasSummaryForMI } from "../modules/rspace/mod";
|
||||||
|
import { getDataSummaryForMI } from "../modules/rdata/mod";
|
||||||
|
|
||||||
|
// ── Types ──
|
||||||
|
|
||||||
|
type KnowledgeCategory =
|
||||||
|
| "tasks" | "time" | "content" | "people" | "social"
|
||||||
|
| "commerce" | "community" | "spatial" | "media" | "infra" | "canvas";
|
||||||
|
|
||||||
|
interface KnowledgeEntry {
|
||||||
|
id: string;
|
||||||
|
moduleId: string;
|
||||||
|
category: KnowledgeCategory;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
tags: string[];
|
||||||
|
timestamp: number;
|
||||||
|
formatted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Category weights (higher = more likely to surface) ──
|
||||||
|
|
||||||
|
const CATEGORY_WEIGHT: Record<KnowledgeCategory, number> = {
|
||||||
|
tasks: 1.5,
|
||||||
|
time: 1.4,
|
||||||
|
content: 1.3,
|
||||||
|
people: 1.2,
|
||||||
|
social: 1.1,
|
||||||
|
commerce: 0.9,
|
||||||
|
community: 0.8,
|
||||||
|
spatial: 0.7,
|
||||||
|
media: 0.7,
|
||||||
|
infra: 0.6,
|
||||||
|
canvas: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const PER_MODULE_LIMIT = 12;
|
||||||
|
|
||||||
|
// ── SpaceKnowledgeIndex ──
|
||||||
|
|
||||||
|
class SpaceKnowledgeIndex {
|
||||||
|
#cache = new Map<string, { entries: KnowledgeEntry[]; builtAt: number }>();
|
||||||
|
|
||||||
|
/** Get ranked context string for injection into MI system prompt. */
|
||||||
|
getRankedContext(space: string, query: string, topN = 18): string {
|
||||||
|
const { entries } = this.#getOrBuild(space);
|
||||||
|
if (entries.length === 0) return "";
|
||||||
|
|
||||||
|
const ranked = this.#rank(entries, query, topN);
|
||||||
|
if (ranked.length === 0) return "";
|
||||||
|
|
||||||
|
// Group by module for readability
|
||||||
|
const groups = new Map<string, string[]>();
|
||||||
|
for (const entry of ranked) {
|
||||||
|
if (!groups.has(entry.moduleId)) groups.set(entry.moduleId, []);
|
||||||
|
groups.get(entry.moduleId)!.push(entry.formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const [moduleId, items] of groups) {
|
||||||
|
lines.push(`- [${moduleId}]`);
|
||||||
|
for (const item of items) {
|
||||||
|
lines.push(` ${item}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `\n- Space data (ranked by relevance):\n${lines.join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invalidate cache for a space (called on doc changes). */
|
||||||
|
invalidate(space: string): void {
|
||||||
|
this.#cache.delete(space);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getOrBuild(space: string): { entries: KnowledgeEntry[]; builtAt: number } {
|
||||||
|
const cached = this.#cache.get(space);
|
||||||
|
if (cached && Date.now() - cached.builtAt < CACHE_TTL) return cached;
|
||||||
|
|
||||||
|
const entries = this.#buildIndex(space);
|
||||||
|
const result = { entries, builtAt: Date.now() };
|
||||||
|
this.#cache.set(space, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildIndex(space: string): KnowledgeEntry[] {
|
||||||
|
const entries: KnowledgeEntry[] = [];
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Each adapter is wrapped in try/catch so one module failure doesn't block others
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const e of getUpcomingEventsForMI(space, 14, PER_MODULE_LIMIT)) {
|
||||||
|
const date = e.allDay ? e.start.split("T")[0] : e.start;
|
||||||
|
let line = `${date}: ${e.title}`;
|
||||||
|
if (e.location) line += ` (${e.location})`;
|
||||||
|
else if (e.isVirtual) line += ` (virtual)`;
|
||||||
|
if (e.tags?.length) line += ` [${e.tags.join(", ")}]`;
|
||||||
|
entries.push({
|
||||||
|
id: `rcal:${e.title}:${e.start}`, moduleId: "rcal", category: "time",
|
||||||
|
title: e.title, detail: `${e.location || ""} ${(e.tags || []).join(" ")}`,
|
||||||
|
tags: e.tags || [], timestamp: Date.parse(e.start) || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const n of getRecentVaultNotesForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${n.title}" in ${n.vaultName} (${n.path})${n.tags.length ? ` [${n.tags.join(", ")}]` : ""}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rnotes:${n.path}`, moduleId: "rnotes", category: "content",
|
||||||
|
title: n.title, detail: `${n.vaultName} ${n.path}`,
|
||||||
|
tags: n.tags, timestamp: now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const t of getRecentTasksForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${t.title}" [${t.status}]${t.priority ? ` (${t.priority})` : ""}${t.description ? `: ${t.description.slice(0, 80)}` : ""}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rtasks:${t.id}`, moduleId: "rtasks", category: "tasks",
|
||||||
|
title: t.title, detail: t.description?.slice(0, 200) || "",
|
||||||
|
tags: [t.status, t.priority].filter(Boolean) as string[],
|
||||||
|
timestamp: t.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const c of getRecentCampaignsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${c.title}" (${c.platforms.join(", ")}, ${c.postCount} posts)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rsocials:${c.id}`, moduleId: "rsocials", category: "social",
|
||||||
|
title: c.title, detail: c.platforms.join(" "),
|
||||||
|
tags: c.platforms, timestamp: c.updatedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const c of getRecentContactsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `${c.name} (${c.role})${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rnetwork:${c.did}`, moduleId: "rnetwork", category: "people",
|
||||||
|
title: c.name, detail: c.role,
|
||||||
|
tags: c.tags, timestamp: now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const t of getRecentThreadsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${t.subject}" from ${t.fromAddress || "unknown"} [${t.status}]${t.isRead ? "" : " (unread)"}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rinbox:${t.subject}`, moduleId: "rinbox", category: "people",
|
||||||
|
title: t.subject, detail: t.fromAddress || "",
|
||||||
|
tags: [t.status, t.isRead ? "" : "unread"].filter(Boolean),
|
||||||
|
timestamp: t.receivedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const c of getRecentCommitmentsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `${c.memberName}: ${c.hours}h ${c.skill} — ${c.desc.slice(0, 80)}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rtime:${c.id}`, moduleId: "rtime", category: "people",
|
||||||
|
title: `${c.memberName} ${c.skill}`, detail: c.desc.slice(0, 200),
|
||||||
|
tags: [c.skill], timestamp: now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const f of getRecentFilesForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `${f.title || f.originalFilename} (${f.mimeType || "unknown"})`;
|
||||||
|
entries.push({
|
||||||
|
id: `rfiles:${f.id}`, moduleId: "rfiles", category: "content",
|
||||||
|
title: f.title || f.originalFilename, detail: f.mimeType || "",
|
||||||
|
tags: [], timestamp: f.updatedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const r of getUpcomingRemindersForMI(space, 14, PER_MODULE_LIMIT)) {
|
||||||
|
const date = new Date(r.remindAt).toISOString().split("T")[0];
|
||||||
|
const line = `${date}: ${r.title}${r.sourceModule ? ` (from ${r.sourceModule})` : ""}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rschedule:${r.id}`, moduleId: "rschedule", category: "time",
|
||||||
|
title: r.title, detail: r.sourceModule || "",
|
||||||
|
tags: [r.sourceModule].filter(Boolean) as string[],
|
||||||
|
timestamp: r.remindAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const p of getMapPinsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${p.label}" (${p.type}) at ${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rmaps:${p.id}`, moduleId: "rmaps", category: "spatial",
|
||||||
|
title: p.label, detail: p.type,
|
||||||
|
tags: [p.type], timestamp: p.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const m of getRecentMeetingsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${m.title}" (${m.participantCount} participants)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rmeets:${m.id}`, moduleId: "rmeets", category: "people",
|
||||||
|
title: m.title, detail: "",
|
||||||
|
tags: [], timestamp: m.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const v of getRecentVideosForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${v.name}" (${v.entryCount} entries)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rtube:${v.id}`, moduleId: "rtube", category: "media",
|
||||||
|
title: v.name, detail: "",
|
||||||
|
tags: [], timestamp: v.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const m of getRecentMessagesForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `[${m.channel}] ${m.author}: ${m.content.slice(0, 80)}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rchats:${m.id}`, moduleId: "rchats", category: "social",
|
||||||
|
title: `${m.channel} ${m.author}`, detail: m.content.slice(0, 200),
|
||||||
|
tags: [m.channel], timestamp: m.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const p of getRecentAgentPostsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `[${p.channel}] ${p.author}: ${p.content.slice(0, 80)}${p.hasPayload ? " [+data]" : ""}`;
|
||||||
|
entries.push({
|
||||||
|
id: `ragents:${p.id}`, moduleId: "ragents", category: "social",
|
||||||
|
title: `${p.channel} ${p.author}`, detail: p.content.slice(0, 200),
|
||||||
|
tags: [p.channel], timestamp: p.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const p of getRecentPublicationsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${p.title}" by ${p.author} (${p.format})`;
|
||||||
|
entries.push({
|
||||||
|
id: `rpubs:${p.id}`, moduleId: "rpubs", category: "content",
|
||||||
|
title: p.title, detail: `${p.author} ${p.format}`,
|
||||||
|
tags: [p.format], timestamp: p.updatedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const d of getRecentDesignsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${d.title}" (${d.productType}, ${d.status})`;
|
||||||
|
entries.push({
|
||||||
|
id: `rswag:${d.id}`, moduleId: "rswag", category: "media",
|
||||||
|
title: d.title, detail: `${d.productType} ${d.status}`,
|
||||||
|
tags: [d.productType, d.status], timestamp: d.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const s of getRecentSheetsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${s.name}" (${s.cellCount} cells)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rsheets:${s.id}`, moduleId: "rsheets", category: "infra",
|
||||||
|
title: s.name, detail: "",
|
||||||
|
tags: [], timestamp: s.updatedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const n of getRecentDocsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${n.title}" (${n.type}${n.tags.length ? `, tags: ${n.tags.join(", ")}` : ""}): ${n.contentPlain.slice(0, 100)}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rdocs:${n.id}`, moduleId: "rdocs", category: "content",
|
||||||
|
title: n.title, detail: n.contentPlain.slice(0, 200),
|
||||||
|
tags: n.tags, timestamp: n.updatedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const s of getRecentSessionsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${s.title}" (${s.pageCount} pages, ${s.frameCount} frames)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rdesign:${s.title}`, moduleId: "rdesign", category: "media",
|
||||||
|
title: s.title, detail: "",
|
||||||
|
tags: [], timestamp: now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const a of getSharedAlbumsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${a.name}"`;
|
||||||
|
entries.push({
|
||||||
|
id: `rphotos:${a.id}`, moduleId: "rphotos", category: "media",
|
||||||
|
title: a.name, detail: "",
|
||||||
|
tags: [], timestamp: a.sharedAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const f of getRecentFlowsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${f.name}" (${f.nodeCount} nodes)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rflows:${f.id}`, moduleId: "rflows", category: "infra",
|
||||||
|
title: f.name, detail: "",
|
||||||
|
tags: [], timestamp: f.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const i of getRecentIntentsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `${i.side} ${i.tokenId} [${i.status}]`;
|
||||||
|
entries.push({
|
||||||
|
id: `rexchange:${i.id}`, moduleId: "rexchange", category: "commerce",
|
||||||
|
title: `${i.side} ${i.tokenId}`, detail: i.status,
|
||||||
|
tags: [i.side, i.status], timestamp: i.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const o of getRecentOrdersForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${o.title}" [${o.status}]`;
|
||||||
|
entries.push({
|
||||||
|
id: `rcart:${o.id}`, moduleId: "rcart", category: "commerce",
|
||||||
|
title: o.title, detail: o.status,
|
||||||
|
tags: [o.status], timestamp: o.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const p of getActiveProposalsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${p.title}" [${p.status}] (${p.voteCount} votes)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rvote:${p.id}`, moduleId: "rvote", category: "community",
|
||||||
|
title: p.title, detail: p.status,
|
||||||
|
tags: [p.status], timestamp: p.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const b of getRecentBooksForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${b.title}" by ${b.author}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rbooks:${b.id}`, moduleId: "rbooks", category: "content",
|
||||||
|
title: b.title, detail: b.author,
|
||||||
|
tags: [b.author], timestamp: b.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const s of getRecentSplatsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${s.title}" (${s.format})`;
|
||||||
|
entries.push({
|
||||||
|
id: `rsplat:${s.id}`, moduleId: "rsplat", category: "media",
|
||||||
|
title: s.title, detail: s.format,
|
||||||
|
tags: [s.format], timestamp: s.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const t of getRecentTripsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${t.title}" [${t.status}] (${t.destinationCount} destinations)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rtrips:${t.id}`, moduleId: "rtrips", category: "spatial",
|
||||||
|
title: t.title, detail: t.status,
|
||||||
|
tags: [t.status], timestamp: t.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const l of getActiveListingsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${l.title}" (${l.type}, ${l.economy})`;
|
||||||
|
entries.push({
|
||||||
|
id: `rbnb:${l.id}`, moduleId: "rbnb", category: "infra",
|
||||||
|
title: l.title, detail: `${l.type} ${l.economy}`,
|
||||||
|
tags: [l.type, l.economy], timestamp: l.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const v of getActiveVehiclesForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${v.title}" (${v.type}, ${v.economy})`;
|
||||||
|
entries.push({
|
||||||
|
id: `rvnb:${v.id}`, moduleId: "rvnb", category: "infra",
|
||||||
|
title: v.title, detail: `${v.type} ${v.economy}`,
|
||||||
|
tags: [v.type, v.economy], timestamp: v.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const i of getForumInstancesForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${i.name}" (${i.domain || "pending"}) [${i.status}]`;
|
||||||
|
entries.push({
|
||||||
|
id: `rforum:${i.id}`, moduleId: "rforum", category: "community",
|
||||||
|
title: i.name, detail: i.domain || "",
|
||||||
|
tags: [i.status], timestamp: i.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const s of getRecentChoiceSessionsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${s.title}" (${s.type}, ${s.optionCount} options)`;
|
||||||
|
entries.push({
|
||||||
|
id: `rchoices:${s.id}`, moduleId: "rchoices", category: "community",
|
||||||
|
title: s.title, detail: s.type,
|
||||||
|
tags: [s.type], timestamp: s.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const p of getActivePromptsForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `"${p.text.slice(0, 60)}" (${p.swipeCount}/${p.threshold})`;
|
||||||
|
entries.push({
|
||||||
|
id: `crowdsurf:${p.id}`, moduleId: "crowdsurf", category: "community",
|
||||||
|
title: p.text.slice(0, 80), detail: "",
|
||||||
|
tags: [], timestamp: p.createdAt || now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const s of getGovShapesForMI(space)) {
|
||||||
|
const line = `${s.type}: ${s.count}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rgov:${s.type}`, moduleId: "rgov", category: "community",
|
||||||
|
title: s.type, detail: `${s.count} shapes`,
|
||||||
|
tags: [s.type], timestamp: now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const t of getCrdtTokensForMI(space, PER_MODULE_LIMIT)) {
|
||||||
|
const line = `${t.symbol} (${t.name}): supply ${t.totalSupply}`;
|
||||||
|
entries.push({
|
||||||
|
id: `rwallet:${t.tokenId}`, moduleId: "rwallet", category: "commerce",
|
||||||
|
title: `${t.symbol} ${t.name}`, detail: `supply ${t.totalSupply}`,
|
||||||
|
tags: [t.symbol], timestamp: now, formatted: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = getCanvasSummaryForMI(space);
|
||||||
|
if (summary.length > 0 && summary[0].totalShapes > 0) {
|
||||||
|
const s = summary[0];
|
||||||
|
const top = s.typeBreakdown.slice(0, 5).map((t) => `${t.type}: ${t.count}`).join(", ");
|
||||||
|
entries.push({
|
||||||
|
id: "rspace:canvas", moduleId: "rspace", category: "canvas",
|
||||||
|
title: "Canvas summary", detail: top,
|
||||||
|
tags: s.typeBreakdown.map((t) => t.type), timestamp: now,
|
||||||
|
formatted: `${s.totalShapes} shapes (${top})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = getDataSummaryForMI(space);
|
||||||
|
if (data.length > 0) {
|
||||||
|
entries.push({
|
||||||
|
id: "rdata:summary", moduleId: "rdata", category: "infra",
|
||||||
|
title: "Analytics", detail: data.map((d) => `${d.label}: ${d.value}`).join(", "),
|
||||||
|
tags: ["analytics"], timestamp: now,
|
||||||
|
formatted: `Analytics: ${data.map((d) => `${d.label}=${d.value}`).join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rank(entries: KnowledgeEntry[], query: string, topN: number): KnowledgeEntry[] {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!query.trim()) {
|
||||||
|
// No query — sort by recency * category weight
|
||||||
|
return entries
|
||||||
|
.map((e) => ({
|
||||||
|
entry: e,
|
||||||
|
score: (0.5 + 0.2 * Math.exp(-(now - e.timestamp) / SEVEN_DAYS)) * CATEGORY_WEIGHT[e.category],
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, topN)
|
||||||
|
.map((r) => r.entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryTrigrams = trigrams(query);
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.map((e) => {
|
||||||
|
const text = `${e.title} ${e.detail} ${e.tags.join(" ")}`;
|
||||||
|
const entryTrigrams = trigrams(text);
|
||||||
|
const sim = jaccardSimilarity(queryTrigrams, entryTrigrams);
|
||||||
|
const recencyBoost = 0.2 * Math.exp(-(now - e.timestamp) / SEVEN_DAYS);
|
||||||
|
const score = (sim + recencyBoost) * CATEGORY_WEIGHT[e.category];
|
||||||
|
return { entry: e, score };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, topN)
|
||||||
|
.map((r) => r.entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const spaceKnowledgeIndex = new SpaceKnowledgeIndex();
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
/**
|
||||||
|
* Per-Space Conversation Memory — persists MI turns to disk per space.
|
||||||
|
*
|
||||||
|
* Lets MI reference past interactions via trigram-ranked retrieval.
|
||||||
|
* Storage: /data/files/space-knowledge/{space}/memory.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { trigrams, jaccardSimilarity } from "./mi-trigrams";
|
||||||
|
|
||||||
|
interface ConversationTurn {
|
||||||
|
query: string;
|
||||||
|
response: string; // first 500 chars
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemoryFile {
|
||||||
|
turns: ConversationTurn[];
|
||||||
|
lastWrittenAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA_ROOT = "/data/files/space-knowledge";
|
||||||
|
const MAX_TURNS = 50;
|
||||||
|
const FLUSH_DELAY = 10_000; // 10s debounce
|
||||||
|
|
||||||
|
class SpaceMemory {
|
||||||
|
#pending = new Map<string, ConversationTurn[]>();
|
||||||
|
#timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
/** Get trigram-ranked past turns relevant to current query. */
|
||||||
|
async getRelevantTurns(space: string, query: string, topN = 3): Promise<string> {
|
||||||
|
const file = await this.#load(space);
|
||||||
|
if (!file || file.turns.length === 0) return "";
|
||||||
|
|
||||||
|
const queryTri = trigrams(query);
|
||||||
|
const scored = file.turns
|
||||||
|
.map((turn) => {
|
||||||
|
const text = `${turn.query} ${turn.response}`;
|
||||||
|
const sim = jaccardSimilarity(queryTri, trigrams(text));
|
||||||
|
return { turn, score: sim };
|
||||||
|
})
|
||||||
|
.filter((s) => s.score > 0.05)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, topN);
|
||||||
|
|
||||||
|
if (scored.length === 0) return "";
|
||||||
|
|
||||||
|
const lines = scored.map((s) => {
|
||||||
|
const date = new Date(s.turn.timestamp).toISOString().split("T")[0];
|
||||||
|
const shortA = s.turn.response.slice(0, 120).replace(/\n/g, " ");
|
||||||
|
return ` - [${date}] Q: ${s.turn.query.slice(0, 80)} -> A: ${shortA}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `\n- Past relevant conversations:\n${lines.join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fire-and-forget append. Debounced 10s flush to disk. */
|
||||||
|
appendTurn(space: string, query: string, response: string): void {
|
||||||
|
const turn: ConversationTurn = {
|
||||||
|
query: query.slice(0, 300),
|
||||||
|
response: response.slice(0, 500),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.#pending.has(space)) this.#pending.set(space, []);
|
||||||
|
this.#pending.get(space)!.push(turn);
|
||||||
|
|
||||||
|
// Debounce flush
|
||||||
|
if (this.#timers.has(space)) clearTimeout(this.#timers.get(space)!);
|
||||||
|
this.#timers.set(space, setTimeout(() => this.#flush(space), FLUSH_DELAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
async #flush(space: string): Promise<void> {
|
||||||
|
const pending = this.#pending.get(space);
|
||||||
|
if (!pending || pending.length === 0) return;
|
||||||
|
this.#pending.delete(space);
|
||||||
|
this.#timers.delete(space);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = (await this.#load(space)) || { turns: [], lastWrittenAt: "" };
|
||||||
|
file.turns.push(...pending);
|
||||||
|
|
||||||
|
// FIFO eviction
|
||||||
|
if (file.turns.length > MAX_TURNS) {
|
||||||
|
file.turns = file.turns.slice(-MAX_TURNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
file.lastWrittenAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const dir = join(DATA_ROOT, space);
|
||||||
|
await mkdir(dir, { recursive: true });
|
||||||
|
await writeFile(join(dir, "memory.json"), JSON.stringify(file, null, 2));
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[space-memory] flush failed for ${space}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #load(space: string): Promise<MemoryFile | null> {
|
||||||
|
try {
|
||||||
|
const raw = await readFile(join(DATA_ROOT, space, "memory.json"), "utf-8");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const spaceMemory = new SpaceMemory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a ReadableStream to capture assistant response text for memory.
|
||||||
|
* Parses NDJSON lines, accumulates content from message chunks.
|
||||||
|
*/
|
||||||
|
export function streamWithMemoryCapture(
|
||||||
|
source: ReadableStream,
|
||||||
|
space: string,
|
||||||
|
query: string,
|
||||||
|
): ReadableStream {
|
||||||
|
let accumulated = "";
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
return source.pipeThrough(
|
||||||
|
new TransformStream({
|
||||||
|
transform(chunk, controller) {
|
||||||
|
// Pass through unchanged
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
|
||||||
|
// Try to extract text content from NDJSON lines
|
||||||
|
try {
|
||||||
|
const text = decoder.decode(chunk, { stream: true });
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
if (parsed.message?.role === "assistant" && parsed.message.content) {
|
||||||
|
accumulated += parsed.message.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-JSON chunk or partial line — ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flush() {
|
||||||
|
// Stream complete — save to memory
|
||||||
|
if (accumulated.length > 10) {
|
||||||
|
spaceMemory.appendTurn(space, query, accumulated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import { SyncServer } from "./local-first/sync-server";
|
import { SyncServer } from "./local-first/sync-server";
|
||||||
import { saveDoc, saveEncryptedBlob, loadEncryptedBlob } from "./local-first/doc-persistence";
|
import { saveDoc, saveEncryptedBlob, loadEncryptedBlob } from "./local-first/doc-persistence";
|
||||||
import { getDocumentData } from "./community-store";
|
import { getDocumentData } from "./community-store";
|
||||||
|
import { spaceKnowledgeIndex } from "./space-knowledge";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up the encryption key ID for a doc's space.
|
* Look up the encryption key ID for a doc's space.
|
||||||
|
|
@ -34,6 +35,8 @@ export const syncServer = new SyncServer({
|
||||||
participantMode: true,
|
participantMode: true,
|
||||||
maxDocs: 500,
|
maxDocs: 500,
|
||||||
onDocChange: (docId, doc) => {
|
onDocChange: (docId, doc) => {
|
||||||
|
const spaceSlug = docId.split(":")[0];
|
||||||
|
if (spaceSlug && spaceSlug !== "global") spaceKnowledgeIndex.invalidate(spaceSlug);
|
||||||
const encryptionKeyId = getEncryptionKeyId(docId);
|
const encryptionKeyId = getEncryptionKeyId(docId);
|
||||||
saveDoc(docId, doc, encryptionKeyId);
|
saveDoc(docId, doc, encryptionKeyId);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue