Add Persona semantic memory recall
This commit is contained in:
parent
a055cc286e
commit
c779f4915f
16 changed files with 396 additions and 12 deletions
|
|
@ -170,14 +170,17 @@ export async function startBot(): Promise<void> {
|
|||
const vectorStores = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: sourceQueryEmbedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
});
|
||||
const vectorStore = vectorStores.history;
|
||||
const sourceRetrievalStore = vectorStores.source;
|
||||
const retrievalService = new RetrievalService(vectorStore, sourceRetrievalStore);
|
||||
const personaMemoryStore = vectorStores.personaMemory;
|
||||
const retrievalService = new RetrievalService(vectorStore, sourceRetrievalStore, personaMemoryStore);
|
||||
const historyIngester = new HistoryIngester();
|
||||
const sourceDocumentIngester = new SourceDocumentIngester();
|
||||
const ragPipeline = new RagPipeline(archiveRepository, historyIngester, vectorStore);
|
||||
|
|
|
|||
|
|
@ -112,19 +112,23 @@ const sourceQueryEmbedder = createTextEmbedder({
|
|||
const vectorStores = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: sourceQueryEmbedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
});
|
||||
const historyVectorStore = vectorStores.history;
|
||||
const sourceVectorStore = vectorStores.source;
|
||||
const personaMemoryVectorStore = vectorStores.personaMemory;
|
||||
const archiveRepository = new FileMessageArchiveRepository(config.ragArchivePath);
|
||||
const sourceArchiveRepository = new FileSourceDocumentArchiveRepository(config.ragSourceArchivePath);
|
||||
const sourceDocumentIngester = new SourceDocumentIngester();
|
||||
const retrievalService = new RetrievalService(
|
||||
historyVectorStore,
|
||||
sourceVectorStore,
|
||||
personaMemoryVectorStore,
|
||||
);
|
||||
|
||||
let isProcessing = false;
|
||||
|
|
|
|||
|
|
@ -47,14 +47,17 @@ export function createVoidbotMcpContext(): VoidbotMcpContext {
|
|||
const vectorStores = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: sourceQueryEmbedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
});
|
||||
const retrievalService = new RetrievalService(
|
||||
vectorStores.history,
|
||||
vectorStores.source,
|
||||
vectorStores.personaMemory,
|
||||
);
|
||||
const sourceDocumentIngester = new SourceDocumentIngester();
|
||||
|
||||
|
|
|
|||
|
|
@ -48,12 +48,14 @@ const envSchema = z.object({
|
|||
DATABASE_DSN: z.string().min(1).default("postgres://voidbot:voidbot@localhost:5432/voidbot"),
|
||||
VECTOR_STORE_KIND: z.enum(["local_json", "qdrant"]).default("local_json"),
|
||||
VECTOR_STORE_PATH: z.string().min(1).default(".voidbot/history-vector-store.json"),
|
||||
PERSONA_MEMORY_VECTOR_STORE_PATH: z.string().min(1).default(".voidbot/persona-memory-vector-store.json"),
|
||||
SOURCE_VECTOR_STORE_ROOT: z.string().min(1).default(".voidbot/source-vectors"),
|
||||
QDRANT_URL: z.string().url().default("http://127.0.0.1:6333"),
|
||||
QDRANT_API_KEY: optionalNonEmptyString,
|
||||
QDRANT_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
|
||||
QDRANT_HISTORY_COLLECTION: z.string().min(1).default("voidbot_discord_history_chunks"),
|
||||
QDRANT_SOURCE_COLLECTION: z.string().min(1).default("voidbot_repository_source_chunks"),
|
||||
QDRANT_PERSONA_MEMORY_COLLECTION: z.string().min(1).default("voidbot_persona_memory_chunks"),
|
||||
RAG_ARCHIVE_PATH: z.string().min(1).default(".voidbot/rag/messages.json"),
|
||||
RAG_SOURCE_ARCHIVE_PATH: z.string().min(1).default(".voidbot/rag/source-documents.json"),
|
||||
RAG_IMPORT_STATE_PATH: z.string().min(1).default(".voidbot/rag/import-state.json"),
|
||||
|
|
@ -253,6 +255,7 @@ export interface AppConfig {
|
|||
vectorStore: {
|
||||
kind: "local_json" | "qdrant";
|
||||
path: string;
|
||||
personaMemoryPath: string;
|
||||
};
|
||||
sourceVectorStoreRoot: string;
|
||||
qdrant: {
|
||||
|
|
@ -261,6 +264,7 @@ export interface AppConfig {
|
|||
timeoutMs: number;
|
||||
historyCollection: string;
|
||||
sourceCollection: string;
|
||||
personaMemoryCollection: string;
|
||||
};
|
||||
rateLimits: {
|
||||
defaultCooldownSeconds: number;
|
||||
|
|
@ -504,6 +508,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|||
vectorStore: {
|
||||
kind: parsed.VECTOR_STORE_KIND,
|
||||
path: resolve(parsed.VECTOR_STORE_PATH),
|
||||
personaMemoryPath: resolve(parsed.PERSONA_MEMORY_VECTOR_STORE_PATH),
|
||||
},
|
||||
sourceVectorStoreRoot: resolve(parsed.SOURCE_VECTOR_STORE_ROOT),
|
||||
qdrant: {
|
||||
|
|
@ -512,6 +517,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|||
timeoutMs: parsed.QDRANT_TIMEOUT_MS,
|
||||
historyCollection: parsed.QDRANT_HISTORY_COLLECTION,
|
||||
sourceCollection: parsed.QDRANT_SOURCE_COLLECTION,
|
||||
personaMemoryCollection: parsed.QDRANT_PERSONA_MEMORY_COLLECTION,
|
||||
},
|
||||
rateLimits: {
|
||||
defaultCooldownSeconds: parsed.VOID_USAGE_COOLDOWN_SECONDS,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ function matchesFilters(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (filters.identityId && metadata.identityId !== filters.identityId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.corpusKind && metadata.corpusKind !== filters.corpusKind) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface StoredQdrantChunk extends EmbeddingChunk {
|
|||
export interface QdrantVectorStoreOptions {
|
||||
url: string;
|
||||
collectionName: string;
|
||||
corpusKind: "discord_history" | "repository_source";
|
||||
corpusKind: "discord_history" | "repository_source" | "persona_memory";
|
||||
apiKey?: string;
|
||||
timeoutMs?: number;
|
||||
embedder?: TextEmbedder;
|
||||
|
|
@ -296,6 +296,10 @@ function buildPayload(chunk: StoredQdrantChunk): QdrantPayload {
|
|||
copyOptionalMetadataField(payload, chunk.metadata, "guildId");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "channelId");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "authorId");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "identityId");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "memoryKind");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "targetKind");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "targetId");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "threadId");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "repoName");
|
||||
copyOptionalMetadataField(payload, chunk.metadata, "language");
|
||||
|
|
@ -346,8 +350,14 @@ function buildPathPrefixes(path: string): string[] {
|
|||
|
||||
function inferCorpusKind(
|
||||
sourceKind: StoredQdrantChunk["sourceKind"],
|
||||
): "discord_history" | "repository_source" {
|
||||
return sourceKind === "source_document" ? "repository_source" : "discord_history";
|
||||
): "discord_history" | "repository_source" | "persona_memory" {
|
||||
if (sourceKind === "source_document") {
|
||||
return "repository_source";
|
||||
}
|
||||
if (sourceKind === "persona_memory") {
|
||||
return "persona_memory";
|
||||
}
|
||||
return "discord_history";
|
||||
}
|
||||
|
||||
function fromQdrantPoint(point: QdrantScoredPoint): RetrievalResult | undefined {
|
||||
|
|
@ -407,8 +417,10 @@ function readString(value: unknown): string | undefined {
|
|||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function readSourceKind(value: unknown): "discord_message" | "source_document" | undefined {
|
||||
return value === "discord_message" || value === "source_document" ? value : undefined;
|
||||
function readSourceKind(value: unknown): "discord_message" | "source_document" | "persona_memory" | undefined {
|
||||
return value === "discord_message" || value === "source_document" || value === "persona_memory"
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function validateEmbeddingBatch(vectors: number[][], expectedCount: number): number {
|
||||
|
|
@ -433,7 +445,7 @@ function validateCollectionCompatibility(
|
|||
collection: Schemas["CollectionInfo"],
|
||||
expected: {
|
||||
collectionName: string;
|
||||
corpusKind: "discord_history" | "repository_source";
|
||||
corpusKind: "discord_history" | "repository_source" | "persona_memory";
|
||||
embedderId: string;
|
||||
vectorLength: number;
|
||||
},
|
||||
|
|
@ -484,7 +496,7 @@ function readCollectionVectorLength(
|
|||
}
|
||||
|
||||
function buildPayloadIndexDefinitions(
|
||||
corpusKind: "discord_history" | "repository_source",
|
||||
corpusKind: "discord_history" | "repository_source" | "persona_memory",
|
||||
): Array<{
|
||||
field_name: string;
|
||||
field_schema:
|
||||
|
|
@ -509,6 +521,16 @@ function buildPayloadIndexDefinitions(
|
|||
];
|
||||
}
|
||||
|
||||
if (corpusKind === "persona_memory") {
|
||||
return [
|
||||
...shared,
|
||||
{ field_name: "identityId", field_schema: "keyword" as const },
|
||||
{ field_name: "memoryKind", field_schema: "keyword" as const },
|
||||
{ field_name: "targetKind", field_schema: "keyword" as const },
|
||||
{ field_name: "targetId", field_schema: "keyword" as const },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...shared,
|
||||
{ field_name: "repoName", field_schema: "keyword" as const },
|
||||
|
|
@ -528,6 +550,7 @@ function toQdrantFilter(filters?: RetrievalFilters): QdrantFilter | undefined {
|
|||
appendExactMatch(must, "guildId", filters.guildId);
|
||||
appendExactMatch(must, "channelId", filters.channelId);
|
||||
appendExactMatch(must, "authorId", filters.authorId);
|
||||
appendExactMatch(must, "identityId", filters.identityId);
|
||||
appendExactMatch(must, "repoName", filters.repoName);
|
||||
appendExactMatch(must, "language", filters.language);
|
||||
appendExactMatch(must, "sourceId", filters.sourceId);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export class RetrievalService {
|
|||
public constructor(
|
||||
private readonly historyVectorStore: VectorStore,
|
||||
private readonly sourceVectorStore: VectorStore = historyVectorStore,
|
||||
private readonly personaMemoryVectorStore: VectorStore = historyVectorStore,
|
||||
) {}
|
||||
|
||||
public async search(
|
||||
|
|
@ -39,4 +40,15 @@ export class RetrievalService {
|
|||
corpusKind: "repository_source",
|
||||
});
|
||||
}
|
||||
|
||||
public async searchPersonaMemory(
|
||||
query: string,
|
||||
limit = 5,
|
||||
filters?: Omit<RetrievalFilters, "corpusKind" | "guildId" | "channelId" | "authorId" | "repoName" | "pathPrefix" | "language">,
|
||||
): Promise<RetrievalResult[]> {
|
||||
return this.personaMemoryVectorStore.query(query, limit, {
|
||||
...filters,
|
||||
corpusKind: "persona_memory",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ShardedSourceVectorStore } from "./source-vector-store";
|
|||
export interface VectorStoreFactoryOptions {
|
||||
kind: "local_json" | "qdrant";
|
||||
historyPath: string;
|
||||
personaMemoryPath: string;
|
||||
sourceRoot: string;
|
||||
qdrant: {
|
||||
url: string;
|
||||
|
|
@ -15,14 +16,17 @@ export interface VectorStoreFactoryOptions {
|
|||
timeoutMs: number;
|
||||
historyCollection: string;
|
||||
sourceCollection: string;
|
||||
personaMemoryCollection: string;
|
||||
};
|
||||
historyEmbedder: TextEmbedder;
|
||||
sourceEmbedder: TextEmbedder;
|
||||
personaMemoryEmbedder: TextEmbedder;
|
||||
}
|
||||
|
||||
export interface BuiltVectorStores {
|
||||
history: VectorStore;
|
||||
source: VectorStore;
|
||||
personaMemory: VectorStore;
|
||||
}
|
||||
|
||||
export function createVectorStores(options: VectorStoreFactoryOptions): BuiltVectorStores {
|
||||
|
|
@ -44,11 +48,20 @@ export function createVectorStores(options: VectorStoreFactoryOptions): BuiltVec
|
|||
corpusKind: "repository_source",
|
||||
embedder: options.sourceEmbedder,
|
||||
}),
|
||||
personaMemory: new QdrantVectorStore({
|
||||
url: options.qdrant.url,
|
||||
apiKey: options.qdrant.apiKey,
|
||||
timeoutMs: options.qdrant.timeoutMs,
|
||||
collectionName: options.qdrant.personaMemoryCollection,
|
||||
corpusKind: "persona_memory",
|
||||
embedder: options.personaMemoryEmbedder,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
history: new FileVectorStore(options.historyPath, options.historyEmbedder),
|
||||
source: new ShardedSourceVectorStore(options.sourceRoot, options.sourceEmbedder),
|
||||
personaMemory: new FileVectorStore(options.personaMemoryPath, options.personaMemoryEmbedder),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,15 +107,16 @@ export interface RetrievalResult {
|
|||
score: number;
|
||||
text: string;
|
||||
sourceId: string;
|
||||
sourceKind: "discord_message" | "source_document";
|
||||
sourceKind: "discord_message" | "source_document" | "persona_memory";
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RetrievalFilters {
|
||||
corpusKind?: "discord_history" | "repository_source";
|
||||
corpusKind?: "discord_history" | "repository_source" | "persona_memory";
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
authorId?: string;
|
||||
identityId?: string;
|
||||
repoName?: string;
|
||||
pathPrefix?: string;
|
||||
language?: string;
|
||||
|
|
@ -591,7 +592,7 @@ export interface ArchivedMessage {
|
|||
export interface EmbeddingChunk {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
sourceKind: "discord_message" | "source_document";
|
||||
sourceKind: "discord_message" | "source_document" | "persona_memory";
|
||||
text: string;
|
||||
normalizedText: string;
|
||||
metadata: Record<string, string>;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,9 @@ Current room weather:
|
|||
|
||||
{{jurisdictionDiveLine}}
|
||||
|
||||
Semantic Persona memory recall:
|
||||
{{semanticMemoryRecallSurface}}
|
||||
|
||||
A new source-grounded opinion, concrete proposal, bylined essay/article plan, agency pressure, playful aside, running joke, or small personal fascination can earn persistence or speech when it adds something real.
|
||||
A living contribution can also be a social or philosophical one: a clearer paraphrase, a felt disagreement, a confession of what the idea does to you, a question that opens the shared mind wider, or a bond-forming reply to another person. Not every useful utterance is a proposal-shaped object.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,10 +48,12 @@ async function main(): Promise<void> {
|
|||
const vectorStore = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: embedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
}).history;
|
||||
const historyIngester = new HistoryIngester();
|
||||
const ragPipeline = new RagPipeline(archiveRepository, historyIngester, vectorStore);
|
||||
|
|
|
|||
|
|
@ -446,10 +446,12 @@ async function markArchiveMessagesDeleted(input: {
|
|||
const vectorStore = createVectorStores({
|
||||
kind: input.config.vectorStore.kind,
|
||||
historyPath: input.config.vectorStore.path,
|
||||
personaMemoryPath: input.config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: input.config.sourceVectorStoreRoot,
|
||||
qdrant: input.config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: embedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
}).history;
|
||||
const pipeline = new RagPipeline(input.archiveRepository, new HistoryIngester(), vectorStore);
|
||||
const deleted: string[] = [];
|
||||
|
|
|
|||
|
|
@ -31,13 +31,16 @@ async function main(): Promise<void> {
|
|||
const vectorStores = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: embedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
});
|
||||
const vectorStore = vectorStores.history;
|
||||
const sourceVectorStore = vectorStores.source;
|
||||
const personaMemoryVectorStore = vectorStores.personaMemory;
|
||||
const historyIngester = new HistoryIngester();
|
||||
const sourceIngester = new SourceDocumentIngester();
|
||||
const activeMessages = await archiveRepository.listAllActive();
|
||||
|
|
@ -47,6 +50,7 @@ async function main(): Promise<void> {
|
|||
|
||||
await vectorStore.clear();
|
||||
await sourceVectorStore.clear();
|
||||
await personaMemoryVectorStore.clear();
|
||||
|
||||
for (let index = 0; index < historyChunks.length; index += REBUILD_CHUNK_BATCH_SIZE) {
|
||||
const batch = historyChunks.slice(index, index + REBUILD_CHUNK_BATCH_SIZE);
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ import {
|
|||
import {
|
||||
createTextEmbedder,
|
||||
createVectorStores,
|
||||
normalizeText,
|
||||
RetrievalService,
|
||||
} from "@voidbot/rag";
|
||||
import {
|
||||
type EmbeddingChunk,
|
||||
loadPromptTemplate,
|
||||
type InteractionMemoryProfile,
|
||||
type PromptImageAttachment,
|
||||
|
|
@ -1736,6 +1738,7 @@ function buildHeartbeatPrompt(input: {
|
|||
channelSnapshots: ChannelSnapshot[];
|
||||
recentMessages: SourceMessage[];
|
||||
memorySurface?: string;
|
||||
semanticMemoryRecallSurface?: string;
|
||||
repoActivitySurface?: string;
|
||||
conversationMemorySurface?: string;
|
||||
humanPronounGuidance?: RepoFaceHumanPronounGuidance[];
|
||||
|
|
@ -1754,6 +1757,7 @@ function buildHeartbeatPrompt(input: {
|
|||
globalAgentDoctrine: input.globalAgentDoctrine,
|
||||
channelId: input.channelId,
|
||||
memorySurface: input.memorySurface ?? `- ${input.identity.displayName} has no strong personal memory surface yet. Let the attached conversation and repo evidence wake something specific.`,
|
||||
semanticMemoryRecallSurface: input.semanticMemoryRecallSurface ?? "- No semantic Persona memory recall was attached for this turn.",
|
||||
repoActivitySurface: input.repoActivitySurface ?? "- No recent home repo activity was attached for this turn.",
|
||||
conversationMemorySurface: input.conversationMemorySurface ?? "- No recent conversation transcript was attached for this turn.",
|
||||
humanPronounDirective: renderRepoFaceHumanPronounFacts(input.humanPronounGuidance ?? [])
|
||||
|
|
@ -1854,6 +1858,12 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
humanPronounGuidance,
|
||||
);
|
||||
const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config);
|
||||
const semanticMemoryRecallSurface = await renderRepoFaceSemanticMemoryRecallForTurn(
|
||||
identity,
|
||||
input.config,
|
||||
memorySurface,
|
||||
roomContext,
|
||||
);
|
||||
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
|
||||
const conversationMemorySurface = input.conversationSurfacePath
|
||||
? await readOptionalMemorySurface(input.conversationSurfacePath)
|
||||
|
|
@ -1875,6 +1885,7 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
channelSnapshots,
|
||||
recentMessages,
|
||||
memorySurface,
|
||||
semanticMemoryRecallSurface,
|
||||
repoActivitySurface,
|
||||
conversationMemorySurface,
|
||||
humanPronounGuidance,
|
||||
|
|
@ -1979,6 +1990,293 @@ async function renderRepoFaceMemorySurfaceForTurn(
|
|||
});
|
||||
}
|
||||
|
||||
async function renderRepoFaceSemanticMemoryRecallForTurn(
|
||||
identity: RepoDiscordIdentity,
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
memorySurface: string,
|
||||
roomContext: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): Promise<string> {
|
||||
try {
|
||||
const statePath = resolveFaceStatePathForSemanticMemory(identity, config);
|
||||
if (!statePath) {
|
||||
return "- Semantic Persona memory recall unavailable: no typed Persona state path is registered.";
|
||||
}
|
||||
const typedState = await loadVoidSelfStateTypedDocuments({
|
||||
canonicalPath: statePath,
|
||||
identity: {
|
||||
agentId: identity.id,
|
||||
publicName: identity.displayName,
|
||||
publicDescription: identity.description,
|
||||
},
|
||||
});
|
||||
const retrieval = createRepoFaceSemanticMemoryRetrievalService(config);
|
||||
const chunks = buildPersonaMemoryChunks(identity, statePath, typedState, memorySurface);
|
||||
const personaMemoryStore = createRepoFacePersonaMemoryVectorStore(config);
|
||||
await personaMemoryStore.deleteByFilters({
|
||||
corpusKind: "persona_memory",
|
||||
identityId: identity.id,
|
||||
});
|
||||
await personaMemoryStore.upsert(chunks);
|
||||
|
||||
const query = buildPersonaMemoryRecallQuery(identity, memorySurface, roomContext);
|
||||
const results = await retrieval.searchPersonaMemory(query, 12, {
|
||||
identityId: identity.id,
|
||||
});
|
||||
|
||||
if (results.length === 0) {
|
||||
return [
|
||||
"- Semantic Persona memory recall ran, but no nearby memories crossed the retrieval threshold.",
|
||||
"- Fall back to the projected state, raw transcript, and direct evidence above.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
return [
|
||||
"These are derived Qdrant/local-vector recall hits from this Persona's typed memory. They are hints, not new authority; the `.cc` state remains the owner.",
|
||||
...results.map((result, index) => {
|
||||
const kind = result.metadata.memoryKind ? `/${result.metadata.memoryKind}` : "";
|
||||
const target = result.metadata.targetLabel ?? result.metadata.targetId ?? "unknown target";
|
||||
return `- ${index + 1}. ${target}${kind} score=${result.score.toFixed(3)}: ${collapseWhitespace(result.text, 520)}`;
|
||||
}),
|
||||
].join("\n");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return [
|
||||
"Semantic Persona memory recall unavailable:",
|
||||
`- ${collapseWhitespace(message, 320)}`,
|
||||
"- Do not pretend semantic Persona memory retrieval was available this turn; use projected state and raw transcript instead.",
|
||||
].join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFaceStatePathForSemanticMemory(
|
||||
identity: RepoDiscordIdentity,
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
): string | undefined {
|
||||
if (identity.identityKind === "native_persona") {
|
||||
return identity.personaStatePath ? resolve(identity.personaStatePath) : undefined;
|
||||
}
|
||||
return resolveRepoFaceStatePath(identity, config.storageRoot);
|
||||
}
|
||||
|
||||
function createRepoFaceSemanticMemoryRetrievalService(config: ReturnType<typeof loadConfig>): RetrievalService {
|
||||
const personaMemoryStore = createRepoFacePersonaMemoryVectorStore(config);
|
||||
return new RetrievalService(personaMemoryStore, personaMemoryStore, personaMemoryStore);
|
||||
}
|
||||
|
||||
function createRepoFacePersonaMemoryVectorStore(config: ReturnType<typeof loadConfig>) {
|
||||
const personaMemoryEmbedder = createTextEmbedder({
|
||||
backend: config.ragEmbeddingBackend,
|
||||
hashDimensions: config.ragEmbeddingDimensions,
|
||||
ollamaBaseUrl: config.ragOllamaBaseUrl,
|
||||
ollamaModel: config.ragOllamaModel,
|
||||
ollamaTimeoutMs: config.ragOllamaTimeoutMs,
|
||||
queryInstruction: "Given a Face turn, retrieve relevant memories, bonds, needs, status reads, and doctrine from this Persona's own typed state.",
|
||||
});
|
||||
return createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: personaMemoryEmbedder,
|
||||
sourceEmbedder: personaMemoryEmbedder,
|
||||
personaMemoryEmbedder,
|
||||
}).personaMemory;
|
||||
}
|
||||
|
||||
function buildPersonaMemoryChunks(
|
||||
identity: RepoDiscordIdentity,
|
||||
statePath: string,
|
||||
state: VoidSelfStateTypedProjection,
|
||||
memorySurface: string,
|
||||
): EmbeddingChunk[] {
|
||||
const chunks: EmbeddingChunk[] = [];
|
||||
const sourceId = `persona:${identity.id}`;
|
||||
const baseMetadata = {
|
||||
corpusKind: "persona_memory",
|
||||
identityId: identity.id,
|
||||
personaName: identity.displayName,
|
||||
sourceId,
|
||||
statePath,
|
||||
};
|
||||
|
||||
const pushChunk = (input: {
|
||||
id: string;
|
||||
kind: string;
|
||||
target?: Record<string, unknown>;
|
||||
text: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}): void => {
|
||||
const text = collapseWhitespace(input.text, 1800);
|
||||
if (text.length < 24) {
|
||||
return;
|
||||
}
|
||||
const target = input.target;
|
||||
chunks.push({
|
||||
id: `${sourceId}:${input.id}`,
|
||||
sourceId,
|
||||
sourceKind: "persona_memory",
|
||||
text,
|
||||
normalizedText: normalizeText(text),
|
||||
metadata: {
|
||||
...baseMetadata,
|
||||
memoryKind: input.kind,
|
||||
targetKind: typeof target?.kind === "string" ? target.kind : "",
|
||||
targetId: typeof target?.id === "string" ? target.id : "",
|
||||
targetLabel: typeof target?.label === "string" ? target.label : "",
|
||||
createdAt: input.createdAt ?? "",
|
||||
updatedAt: input.updatedAt ?? "",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
pushChunk({
|
||||
id: "projected-surface",
|
||||
kind: "projected_surface",
|
||||
target: { kind: "self", id: identity.id, label: identity.displayName },
|
||||
text: memorySurface,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
for (const value of state.selfProfile.values) {
|
||||
pushChunk({
|
||||
id: `value:${value.id}`,
|
||||
kind: "value",
|
||||
target: { kind: "self", id: identity.id, label: identity.displayName },
|
||||
text: joinMemoryFields(value.label, value.summary),
|
||||
});
|
||||
}
|
||||
|
||||
for (const memory of [...state.thoughtMemory.memories, ...state.thoughtMemory.shortTerm]) {
|
||||
if (memory.retiredAt) {
|
||||
continue;
|
||||
}
|
||||
pushChunk({
|
||||
id: `memory:${memory.memoryId}`,
|
||||
kind: memory.kind,
|
||||
target: memory.target,
|
||||
text: joinMemoryFields(memory.summary, memory.claim, memory.question, memory.tension, memory.actionImplication),
|
||||
createdAt: memory.createdAt,
|
||||
updatedAt: memory.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const pressure of state.agencyPressure.pressures) {
|
||||
if (pressure.status === "retired") {
|
||||
continue;
|
||||
}
|
||||
pushChunk({
|
||||
id: `agency:${pressure.pressureId}`,
|
||||
kind: `agency:${pressure.kind}:${pressure.status}`,
|
||||
target: pressure.target,
|
||||
text: joinMemoryFields(pressure.summary, pressure.claim, pressure.question, pressure.tension, pressure.actionImplication),
|
||||
createdAt: pressure.createdAt,
|
||||
updatedAt: pressure.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const need of state.faceAffect.needs) {
|
||||
if (need.status === "retired") {
|
||||
continue;
|
||||
}
|
||||
pushChunk({
|
||||
id: `need:${need.needId}`,
|
||||
kind: `need:${need.kind}:${need.status}`,
|
||||
target: need.target,
|
||||
text: joinMemoryFields(need.summary, need.claim, need.question, need.tension, need.actionImplication),
|
||||
createdAt: need.createdAt,
|
||||
updatedAt: need.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const bond of state.faceAffect.socialBonds) {
|
||||
if (bond.status !== "active") {
|
||||
continue;
|
||||
}
|
||||
pushChunk({
|
||||
id: `bond:${bond.bondId}`,
|
||||
kind: `bond:${bond.stance}`,
|
||||
target: bond.target,
|
||||
text: joinMemoryFields(bond.summary, bond.claim, bond.tension, bond.actionImplication),
|
||||
createdAt: bond.createdAt,
|
||||
updatedAt: bond.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const read of state.faceAffect.statusReads) {
|
||||
if (read.retiredAt) {
|
||||
continue;
|
||||
}
|
||||
pushChunk({
|
||||
id: `status:${read.readId}`,
|
||||
kind: `status:${read.status}`,
|
||||
target: read.target,
|
||||
text: joinMemoryFields(read.summary, read.claim, read.tension, read.actionImplication),
|
||||
createdAt: read.createdAt,
|
||||
updatedAt: read.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const stance of state.faceAffect.doctrineStances) {
|
||||
if (stance.status === "retired") {
|
||||
continue;
|
||||
}
|
||||
pushChunk({
|
||||
id: `doctrine:${stance.stanceId}`,
|
||||
kind: `doctrine:${stance.doctrine}:${stance.status}`,
|
||||
target: stance.target,
|
||||
text: joinMemoryFields(stance.summary, stance.claim, stance.question, stance.tension, stance.actionImplication),
|
||||
createdAt: stance.createdAt,
|
||||
updatedAt: stance.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function buildPersonaMemoryRecallQuery(
|
||||
identity: RepoDiscordIdentity,
|
||||
memorySurface: string,
|
||||
roomContext: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): string {
|
||||
const recentRoomText = roomContext.recentMessages
|
||||
.slice(-8)
|
||||
.map((message) => `${message.authorName}: ${collapseWhitespace(message.content, 360)}`)
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.join("\n");
|
||||
const nearbyText = roomContext.channelSnapshots
|
||||
.flatMap((snapshot) => snapshot.messages.slice(-3).map((message) =>
|
||||
`${snapshot.label ?? snapshot.channelId} / ${message.authorName}: ${collapseWhitespace(message.content, 260)}`,
|
||||
))
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.slice(-12)
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
`${identity.displayName} current Face turn`,
|
||||
"Projected memory surface:",
|
||||
collapseWhitespace(memorySurface, 2200),
|
||||
"Recent room:",
|
||||
recentRoomText,
|
||||
"Nearby room texture:",
|
||||
nearbyText,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function joinMemoryFields(...fields: Array<string | undefined>): string {
|
||||
return fields
|
||||
.map((field) => cleanCharacterFacingSentence(field))
|
||||
.filter((field) => field.length > 0)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
async function renderNativePersonaMemorySurface(
|
||||
identity: RepoDiscordIdentity,
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
|
|
@ -2725,12 +3023,14 @@ function createRepoFaceCuriosityRetrievalService(config: ReturnType<typeof loadC
|
|||
const stores = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder,
|
||||
sourceEmbedder,
|
||||
personaMemoryEmbedder: historyEmbedder,
|
||||
});
|
||||
return new RetrievalService(stores.history, stores.source);
|
||||
return new RetrievalService(stores.history, stores.source, stores.personaMemory);
|
||||
}
|
||||
|
||||
function buildRepoFaceCuriositySeedQueries(
|
||||
|
|
|
|||
|
|
@ -85,10 +85,12 @@ async function main(): Promise<void> {
|
|||
const vectorStore = createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: embedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
}).history;
|
||||
const ragPipeline = new RagPipeline(
|
||||
archiveRepository,
|
||||
|
|
|
|||
|
|
@ -41,10 +41,12 @@ export function createSourceVectorStore(config: AppConfig) {
|
|||
return createVectorStores({
|
||||
kind: config.vectorStore.kind,
|
||||
historyPath: config.vectorStore.path,
|
||||
personaMemoryPath: config.vectorStore.personaMemoryPath,
|
||||
sourceRoot: config.sourceVectorStoreRoot,
|
||||
qdrant: config.qdrant,
|
||||
historyEmbedder: embedder,
|
||||
sourceEmbedder: embedder,
|
||||
personaMemoryEmbedder: embedder,
|
||||
}).source;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue