Add semantic Persona response pressure
This commit is contained in:
parent
7202a41d9e
commit
b0dbb2d8bd
12 changed files with 483 additions and 6 deletions
|
|
@ -264,6 +264,8 @@ function buildUpcomingTurns(participants) {
|
|||
nextTurnInMinutes: participant.nextTurnInMinutes,
|
||||
effectiveSpeed: participant.effectiveSpeed,
|
||||
heat: participant.heat,
|
||||
dynamicHeat: participant.dynamicHeat,
|
||||
responsePressure: participant.responsePressure,
|
||||
lane: index % 3,
|
||||
timelinePosition: participant.activeJobId ? 0 : Math.min(100, Math.max(0, ((Math.max(0, participant.nextTurnInMinutes ?? 0)) / maxMinutes) * 100)),
|
||||
}));
|
||||
|
|
@ -331,6 +333,16 @@ function projectParticipant(participant, pendingMentions, initiativeClock, ident
|
|||
status: String(participant.status ?? "unknown"),
|
||||
currentLoad: numberOrNull(participant.currentLoad),
|
||||
heat: numberOrNull(participant.heat),
|
||||
dynamicHeat: numberOrNull(participant.dynamicHeat),
|
||||
responsePressure: numberOrNull(participant.responsePressure),
|
||||
responsePressureEvidence: Array.isArray(participant.responsePressureEvidence)
|
||||
? participant.responsePressureEvidence.slice(0, 6).map((entry) => ({
|
||||
messageId: stringOrNull(entry?.messageId),
|
||||
observedAt: stringOrNull(entry?.observedAt),
|
||||
similarity: numberOrNull(entry?.similarity),
|
||||
contribution: numberOrNull(entry?.contribution),
|
||||
}))
|
||||
: [],
|
||||
effectiveSpeed: numberOrNull(participant.effectiveSpeed),
|
||||
initiativeSpeed: numberOrNull(participant.initiativeSpeed),
|
||||
nextTurnAt,
|
||||
|
|
@ -2431,6 +2443,8 @@ function renderHtml(snapshot) {
|
|||
statBar("Memory", number(memoryCount), Math.min(100, memoryCount * 7), "cool") +
|
||||
statBar("Pressure", number(counts.pressures || 0), Math.min(100, (counts.pressures || 0) * 14), (counts.pressures || 0) > 4 ? "hot" : "") +
|
||||
statBar("Heat", number(agent.heat), Math.min(100, (agent.heat || 0) * 34), (agent.heat || 0) > 1.5 ? "hot" : "") +
|
||||
statBar("Response", number(agent.responsePressure), Math.min(100, (agent.responsePressure || 0) * 100), (agent.responsePressure || 0) > 0.6 ? "hot" : "") +
|
||||
statBar("Dynamic", number(agent.dynamicHeat), Math.min(100, (agent.dynamicHeat || 0) * 40), (agent.dynamicHeat || 0) > 1.5 ? "hot" : "") +
|
||||
statBar("Load", number(agent.currentLoad), Math.min(100, (agent.currentLoad || 0) * 100), (agent.currentLoad || 0) > 0.7 ? "hot" : "") +
|
||||
statBar("Speed", number(agent.effectiveSpeed), Math.min(100, (agent.effectiveSpeed || 0) * 34), "") +
|
||||
"</div><div class=\\"inspector-lore\\"><p>" + esc(agent.description || "No Face description registered.") + "</p><div class=\\"channel-list\\">" + (agent.channelPermissions || []).map((channel) => "<div class=\\"channel-chip\\"><strong>" + esc(channel.label || "channel") + "</strong><span>x" + esc(number(channel.speedMultiplier || 1)) + "</span><span class=\\"muted\\">" + esc(channel.topic || "no topic") + "</span><span class=\\"mono muted\\">" + esc(channel.speechThreshold || "threshold") + "</span></div>").join("") + "</div></div></section>";
|
||||
|
|
@ -2767,6 +2781,7 @@ function redactSnapshot(snapshot) {
|
|||
...participant,
|
||||
activeJobId: participant.activeJobId ? "active" : null,
|
||||
constraints: [],
|
||||
responsePressureEvidence: [],
|
||||
constraintCount: participant.constraintCount,
|
||||
description: participant.description ? truncate(participant.description, 220) : null,
|
||||
faceStatePath: null,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
loadFaceIdentityRegistry,
|
||||
loadVoidSelfStateTypedDocuments,
|
||||
REPO_FACE_HEARTBEAT_SCHEMA_VERSION,
|
||||
projectRepoFaceResponsePressure,
|
||||
type RepoFaceRestSnapshot,
|
||||
resolveRepoFaceStatePath,
|
||||
type RepoFacePendingMention,
|
||||
|
|
@ -29,8 +30,10 @@ import {
|
|||
import {
|
||||
createTextEmbedder,
|
||||
createVectorStores,
|
||||
embedAffinityCards,
|
||||
normalizeText,
|
||||
RetrievalService,
|
||||
scoreAffinityMessages,
|
||||
} from "@voidbot/rag";
|
||||
import {
|
||||
type EmbeddingChunk,
|
||||
|
|
@ -61,6 +64,15 @@ interface FaceHeartbeatParticipant {
|
|||
status: "active" | "blocked" | "withdrawn" | "offscreen";
|
||||
groups: string[];
|
||||
heat: number;
|
||||
dynamicHeat: number;
|
||||
responsePressure: number;
|
||||
responsePressureEvidence: Array<{
|
||||
messageId: string;
|
||||
observedAt: string;
|
||||
similarity: number;
|
||||
contribution: number;
|
||||
}>;
|
||||
semanticInterruptReceipts: string[];
|
||||
effectiveSpeed: number;
|
||||
baseRecoveryMinutes: number;
|
||||
nextTurnAt: number;
|
||||
|
|
@ -99,6 +111,7 @@ interface IdleCoolingSnapshot {
|
|||
recoveryMinutes: number;
|
||||
lastUnpromptedTurnQueuedAt?: string;
|
||||
nextUnpromptedTurnAllowedAt?: string;
|
||||
observedHumanMessages: Array<SourceMessage & { channelId: string }>;
|
||||
}
|
||||
|
||||
interface StaleActiveTurn {
|
||||
|
|
@ -269,6 +282,15 @@ async function main(): Promise<void> {
|
|||
state,
|
||||
now,
|
||||
});
|
||||
await applySemanticResponsePressure({
|
||||
state,
|
||||
specs: buildParticipantSpecs(registry.identities),
|
||||
messages: idleCooling.observedHumanMessages,
|
||||
restStates,
|
||||
completedThisTick,
|
||||
config,
|
||||
now,
|
||||
});
|
||||
|
||||
const selected = selectReadyParticipants(
|
||||
state,
|
||||
|
|
@ -297,6 +319,8 @@ async function main(): Promise<void> {
|
|||
nextTurnAt: participant.nextTurnAt,
|
||||
recoveryMinutes,
|
||||
heat: participant.heat,
|
||||
dynamicHeat: participant.dynamicHeat,
|
||||
responsePressure: participant.responsePressure,
|
||||
effectiveSpeed: participant.effectiveSpeed,
|
||||
pendingMentionCount: pendingMentions.length,
|
||||
});
|
||||
|
|
@ -366,6 +390,8 @@ async function main(): Promise<void> {
|
|||
initiativeClock: state.initiativeClock,
|
||||
frozen: true,
|
||||
heat: participant.heat,
|
||||
dynamicHeat: participant.dynamicHeat,
|
||||
responsePressure: participant.responsePressure,
|
||||
effectiveSpeed: participant.effectiveSpeed,
|
||||
pendingMentionCount: pendingMentions.length,
|
||||
});
|
||||
|
|
@ -382,6 +408,8 @@ async function main(): Promise<void> {
|
|||
initiativeClock: state.initiativeClock,
|
||||
reason: turn.failureReason,
|
||||
heat: participant.heat,
|
||||
dynamicHeat: participant.dynamicHeat,
|
||||
responsePressure: participant.responsePressure,
|
||||
effectiveSpeed: participant.effectiveSpeed,
|
||||
});
|
||||
}
|
||||
|
|
@ -406,7 +434,11 @@ async function main(): Promise<void> {
|
|||
selected: selected.map((entry) => entry.identityId),
|
||||
queued: queuedIdentityIds,
|
||||
statePath: config.repoFaceHeartbeats.statePath,
|
||||
idleCooling,
|
||||
idleCooling: {
|
||||
...idleCooling,
|
||||
observedHumanMessages: undefined,
|
||||
observedHumanMessageCount: idleCooling.observedHumanMessages.length,
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
|
|
@ -1457,7 +1489,8 @@ function reconcileParticipants(
|
|||
const speed = initiativeSpeedFor(spec, speedOverrides) * spec.channelSpeedMultiplier;
|
||||
const groups = initiativeGroupsFor(spec);
|
||||
const heat = heatFor(spec, groups, globalHeat, heatOverrides);
|
||||
const effectiveSpeed = clamp(speed * heat, 0.1, 12);
|
||||
const dynamicHeat = Number.isFinite(current?.dynamicHeat) ? current.dynamicHeat : 1;
|
||||
const effectiveSpeed = clamp(speed * heat * dynamicHeat, 0.1, 12);
|
||||
const nextTurnAt = Number.isFinite(current?.nextTurnAt)
|
||||
? current.nextTurnAt
|
||||
: initiativeClock + ((baseRecoveryMinutes / count) * index);
|
||||
|
|
@ -1471,6 +1504,10 @@ function reconcileParticipants(
|
|||
initiativeSpeed: speed,
|
||||
groups,
|
||||
heat,
|
||||
dynamicHeat,
|
||||
responsePressure: Number.isFinite(current.responsePressure) ? current.responsePressure : 0,
|
||||
responsePressureEvidence: Array.isArray(current.responsePressureEvidence) ? current.responsePressureEvidence : [],
|
||||
semanticInterruptReceipts: Array.isArray(current.semanticInterruptReceipts) ? current.semanticInterruptReceipts : [],
|
||||
effectiveSpeed,
|
||||
baseRecoveryMinutes,
|
||||
nextTurnAt,
|
||||
|
|
@ -1502,6 +1539,10 @@ function reconcileParticipants(
|
|||
status: hasChannel ? "active" : "blocked",
|
||||
groups,
|
||||
heat,
|
||||
dynamicHeat: 1,
|
||||
responsePressure: 0,
|
||||
responsePressureEvidence: [],
|
||||
semanticInterruptReceipts: [],
|
||||
effectiveSpeed,
|
||||
baseRecoveryMinutes,
|
||||
nextTurnAt,
|
||||
|
|
@ -1624,6 +1665,142 @@ function selectReadyParticipants(
|
|||
return [...mentioned, ...cooled].slice(0, maxJobs);
|
||||
}
|
||||
|
||||
async function applySemanticResponsePressure(input: {
|
||||
state: FaceHeartbeatState;
|
||||
specs: ParticipantSpec[];
|
||||
messages: Array<SourceMessage & { channelId: string }>;
|
||||
restStates: Map<string, RepoFaceRestSnapshot>;
|
||||
completedThisTick: Set<string>;
|
||||
config: ReturnType<typeof loadConfig>;
|
||||
now: Date;
|
||||
}): Promise<void> {
|
||||
const specs = input.specs.filter((spec) => {
|
||||
const participant = input.state.participants.find((entry) => entry.identityId === spec.id);
|
||||
return Boolean(
|
||||
spec.identity &&
|
||||
spec.participantKind !== "system_agent" &&
|
||||
participant?.status === "active" &&
|
||||
participant.currentLoad < 1 &&
|
||||
!input.completedThisTick.has(participant.identityId) &&
|
||||
input.restStates.get(participant.identityId)?.isNapping !== true,
|
||||
);
|
||||
});
|
||||
const recentMessages = input.messages
|
||||
.filter((message) => !message.isBot && message.content.trim().length > 0)
|
||||
.filter((message) => input.now.getTime() - Date.parse(message.timestamp) <= 3 * 60 * 60_000)
|
||||
.sort((left, right) => right.timestamp.localeCompare(left.timestamp))
|
||||
.slice(0, 24);
|
||||
|
||||
if (specs.length === 0 || recentMessages.length === 0) {
|
||||
for (const participant of input.state.participants) {
|
||||
participant.dynamicHeat = 1;
|
||||
participant.responsePressure = 0;
|
||||
participant.responsePressureEvidence = [];
|
||||
participant.effectiveSpeed = clamp(participant.initiativeSpeed * participant.heat, 0.1, 12);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const embedder = createTextEmbedder({
|
||||
backend: input.config.ragEmbeddingBackend,
|
||||
hashDimensions: input.config.ragEmbeddingDimensions,
|
||||
ollamaBaseUrl: input.config.ragOllamaBaseUrl,
|
||||
ollamaModel: input.config.ragOllamaModel,
|
||||
ollamaTimeoutMs: input.config.ragOllamaTimeoutMs,
|
||||
});
|
||||
const cards = await embedAffinityCards(embedder, specs.map((spec) => ({
|
||||
id: spec.id,
|
||||
text: renderInitiativeAffinityCard(spec),
|
||||
})));
|
||||
const scores = await scoreAffinityMessages(embedder, cards, recentMessages.map((message) => ({
|
||||
id: message.id,
|
||||
text: message.content,
|
||||
observedAt: message.timestamp,
|
||||
})));
|
||||
const projections = projectRepoFaceResponsePressure({
|
||||
participants: input.state.participants
|
||||
.filter((participant) => specs.some((spec) => spec.id === participant.identityId))
|
||||
.map((participant) => ({
|
||||
identityId: participant.identityId,
|
||||
interruptThreshold: participant.interruptThreshold,
|
||||
})),
|
||||
evidence: scores.flatMap((score) => {
|
||||
const spec = specs.find((entry) => entry.id === score.cardId);
|
||||
const message = recentMessages.find((entry) => entry.id === score.messageId);
|
||||
if (!spec || !message || !spec.allowedChannelIds.includes(message.channelId)) {
|
||||
return [];
|
||||
}
|
||||
return [{
|
||||
identityId: score.cardId,
|
||||
messageId: score.messageId,
|
||||
observedAt: score.observedAt,
|
||||
similarity: score.similarity,
|
||||
}];
|
||||
}),
|
||||
now: input.now,
|
||||
maxAwakened: Math.min(3, specs.length),
|
||||
});
|
||||
|
||||
for (const participant of input.state.participants) {
|
||||
const projection = projections.find((entry) => entry.identityId === participant.identityId);
|
||||
participant.responsePressure = projection?.pressure ?? 0;
|
||||
participant.responsePressureEvidence = projection?.evidence ?? [];
|
||||
participant.dynamicHeat = clamp(1 + participant.responsePressure * 1.5, 1, 2.5);
|
||||
participant.effectiveSpeed = clamp(
|
||||
participant.initiativeSpeed * participant.heat * participant.dynamicHeat,
|
||||
0.1,
|
||||
12,
|
||||
);
|
||||
const unseenInterruptEvidence = projection?.interrupt
|
||||
? projection.evidence.find((entry) => !participant.semanticInterruptReceipts.includes(entry.messageId))
|
||||
: undefined;
|
||||
if (unseenInterruptEvidence && participant.currentLoad < 1) {
|
||||
participant.nextTurnAt = Math.min(participant.nextTurnAt, input.state.initiativeClock);
|
||||
participant.semanticInterruptReceipts = mergeStrings(
|
||||
participant.semanticInterruptReceipts,
|
||||
unseenInterruptEvidence.messageId,
|
||||
).slice(-40);
|
||||
input.state.history.push({
|
||||
type: "semantic_response_interrupt",
|
||||
identityId: participant.identityId,
|
||||
messageId: unseenInterruptEvidence.messageId,
|
||||
pressure: participant.responsePressure,
|
||||
similarity: unseenInterruptEvidence.similarity,
|
||||
contribution: unseenInterruptEvidence.contribution,
|
||||
observedAt: unseenInterruptEvidence.observedAt,
|
||||
projectedAt: input.now.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
for (const participant of input.state.participants) {
|
||||
participant.dynamicHeat = 1;
|
||||
participant.responsePressure = 0;
|
||||
participant.responsePressureEvidence = [];
|
||||
participant.effectiveSpeed = clamp(participant.initiativeSpeed * participant.heat, 0.1, 12);
|
||||
}
|
||||
input.state.history.push({
|
||||
type: "semantic_response_pressure_unavailable",
|
||||
observedAt: input.now.toISOString(),
|
||||
reason: collapseWhitespace(error instanceof Error ? error.message : String(error), 300),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderInitiativeAffinityCard(spec: ParticipantSpec): string {
|
||||
const identity = spec.identity;
|
||||
return [
|
||||
`${spec.displayName} is the Persona steward of ${spec.repoName}.`,
|
||||
identity?.description,
|
||||
...(identity?.channelPermissions ?? []).flatMap((permission) => [
|
||||
permission.topic,
|
||||
permission.label,
|
||||
permission.posture,
|
||||
]),
|
||||
].filter((value): value is string => typeof value === "string" && value.trim().length > 0).join("\n");
|
||||
}
|
||||
|
||||
function advanceInitiativeClockFromWallClock(state: FaceHeartbeatState, now: Date): void {
|
||||
const lastTickMs = Date.parse(state.lastTickAt ?? "");
|
||||
if (!Number.isFinite(lastTickMs)) {
|
||||
|
|
@ -1710,11 +1887,9 @@ async function readIdleCoolingSnapshot(input: {
|
|||
idleAfterMinutes: policy.idleAfterMinutes,
|
||||
recoveryMinutes: policy.recoveryMinutes,
|
||||
lastUnpromptedTurnQueuedAt: newestUnpromptedTurnQueuedAt(input.state),
|
||||
observedHumanMessages: [] as Array<SourceMessage & { channelId: string }>,
|
||||
};
|
||||
|
||||
if (!policy.enabled) {
|
||||
return { ...base, reason: "disabled" };
|
||||
}
|
||||
if (!input.config.botToken) {
|
||||
return { ...base, reason: "missing_discord_bot_token" };
|
||||
}
|
||||
|
|
@ -1735,6 +1910,9 @@ async function readIdleCoolingSnapshot(input: {
|
|||
if (message.isBot || !message.content.trim()) {
|
||||
continue;
|
||||
}
|
||||
if (!base.observedHumanMessages.some((entry) => entry.id === message.id)) {
|
||||
base.observedHumanMessages.push({ ...message, channelId });
|
||||
}
|
||||
const timestampMs = Date.parse(message.timestamp);
|
||||
const newestMs = Date.parse(newestHumanActivityAt ?? "");
|
||||
if (Number.isFinite(timestampMs) && (!Number.isFinite(newestMs) || timestampMs > newestMs)) {
|
||||
|
|
@ -1752,6 +1930,14 @@ async function readIdleCoolingSnapshot(input: {
|
|||
? new Date(lastUnpromptedTurnQueuedMs + policy.recoveryMinutes * 60_000).toISOString()
|
||||
: undefined;
|
||||
|
||||
if (!policy.enabled) {
|
||||
return {
|
||||
...base,
|
||||
reason: fetchErrors.length > 0 ? "disabled_with_activity_fetch_failure" : "disabled",
|
||||
lastHumanActivityAt: newestHumanActivityAt,
|
||||
};
|
||||
}
|
||||
|
||||
if (!newestHumanActivityAt) {
|
||||
return {
|
||||
...base,
|
||||
|
|
@ -5584,6 +5770,14 @@ function migrateLegacyHeartbeatState(
|
|||
status: participant.status ?? "active",
|
||||
groups: participant.groups ?? [],
|
||||
heat: Number.isFinite(participant.heat) ? participant.heat : 1,
|
||||
dynamicHeat: Number.isFinite(participant.dynamicHeat) ? participant.dynamicHeat : 1,
|
||||
responsePressure: Number.isFinite(participant.responsePressure) ? participant.responsePressure : 0,
|
||||
responsePressureEvidence: Array.isArray(participant.responsePressureEvidence)
|
||||
? participant.responsePressureEvidence
|
||||
: [],
|
||||
semanticInterruptReceipts: Array.isArray(participant.semanticInterruptReceipts)
|
||||
? participant.semanticInterruptReceipts
|
||||
: [],
|
||||
effectiveSpeed: Number.isFinite(participant.effectiveSpeed) ? participant.effectiveSpeed : speed,
|
||||
baseRecoveryMinutes,
|
||||
nextTurnAt: minutesUntilReady,
|
||||
|
|
|
|||
99
scripts/smoke-repo-face-response-pressure.ts
Normal file
99
scripts/smoke-repo-face-response-pressure.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import assert from "node:assert/strict";
|
||||
|
||||
import { projectRepoFaceResponsePressure } from "@voidbot/core";
|
||||
import {
|
||||
embedAffinityCards,
|
||||
scoreAffinityMessages,
|
||||
type TextEmbedder,
|
||||
} from "@voidbot/rag";
|
||||
|
||||
const now = new Date("2026-07-12T12:00:00.000Z");
|
||||
const embedder: TextEmbedder = {
|
||||
id: "fixture:3",
|
||||
dimensions: 3,
|
||||
async embedDocuments(texts) {
|
||||
return texts.map(vectorFor);
|
||||
},
|
||||
async embedQuery(text) {
|
||||
return vectorFor(text);
|
||||
},
|
||||
};
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const cards = await embedAffinityCards(embedder, [
|
||||
{ id: "aqua", text: "music synthesizer sound patch" },
|
||||
{ id: "heimdall", text: "oauth custody authentication grants" },
|
||||
{ id: "nibu", text: "aetheria lore narrative factions" },
|
||||
]);
|
||||
const scores = await scoreAffinityMessages(embedder, cards, [
|
||||
{ id: "music", text: "the synthesizer patch has a strange sound", observedAt: now.toISOString() },
|
||||
{ id: "auth", text: "oauth grants need revocation", observedAt: now.toISOString() },
|
||||
]);
|
||||
|
||||
assert.equal(cards.length, 3);
|
||||
const score = (cardId: string, messageId: string): number =>
|
||||
scores.find((item) => item.cardId === cardId && item.messageId === messageId)?.similarity ?? -1;
|
||||
assert.ok(score("aqua", "music") > score("heimdall", "music"));
|
||||
assert.ok(score("heimdall", "auth") > score("aqua", "auth"));
|
||||
|
||||
const projected = projectRepoFaceResponsePressure({
|
||||
participants: cards.map((card) => ({ identityId: card.id, interruptThreshold: 0.5 })),
|
||||
evidence: scores.map((item) => ({
|
||||
identityId: item.cardId,
|
||||
messageId: item.messageId,
|
||||
observedAt: item.observedAt,
|
||||
similarity: item.similarity,
|
||||
})),
|
||||
now,
|
||||
maxAwakened: 2,
|
||||
});
|
||||
assert.ok((projected.find((item) => item.identityId === "aqua")?.pressure ?? 0) > 0.5);
|
||||
assert.ok((projected.find((item) => item.identityId === "heimdall")?.pressure ?? 0) > 0.5);
|
||||
assert.equal(projected.find((item) => item.identityId === "nibu")?.pressure, 0, "dogpile cap must cool irrelevant cards");
|
||||
|
||||
const expired = projectRepoFaceResponsePressure({
|
||||
participants: [{ identityId: "aqua", interruptThreshold: 0.5 }],
|
||||
evidence: [{
|
||||
identityId: "aqua",
|
||||
messageId: "old",
|
||||
observedAt: "2026-07-12T06:00:00.000Z",
|
||||
similarity: 1,
|
||||
}],
|
||||
now,
|
||||
ttlMinutes: 60,
|
||||
});
|
||||
assert.equal(expired[0]?.pressure, 0, "expired evidence must not alter initiative");
|
||||
|
||||
const duplicate = projectRepoFaceResponsePressure({
|
||||
participants: [{ identityId: "aqua", interruptThreshold: 0.5 }],
|
||||
evidence: [
|
||||
{ identityId: "aqua", messageId: "same", observedAt: now.toISOString(), similarity: 0.8 },
|
||||
{ identityId: "aqua", messageId: "same", observedAt: now.toISOString(), similarity: 0.8 },
|
||||
],
|
||||
now,
|
||||
});
|
||||
assert.equal(duplicate[0]?.evidence.length, 1, "replayed windows must be idempotent by message id");
|
||||
|
||||
await assert.rejects(
|
||||
scoreAffinityMessages({ ...embedder, id: "other" }, cards, []),
|
||||
/uses fixture:3, not other/,
|
||||
);
|
||||
|
||||
process.stdout.write(`${JSON.stringify({ ok: true, cards: cards.length, scoreCount: scores.length, projected })}\n`);
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
function vectorFor(text: string): number[] {
|
||||
const lower = text.toLowerCase();
|
||||
const vector = [
|
||||
/music|synth|sound|patch/.test(lower) ? 1 : 0,
|
||||
/oauth|auth|grant|custody|revocation/.test(lower) ? 1 : 0,
|
||||
/aetheria|lore|narrative|faction/.test(lower) ? 1 : 0,
|
||||
];
|
||||
const magnitude = Math.sqrt(vector.reduce((sum, value) => sum + value ** 2, 0));
|
||||
return magnitude > 0 ? vector.map((value) => value / magnitude) : vector;
|
||||
}
|
||||
Loading…
Reference in a new issue