Add private profile disclosure command
This commit is contained in:
parent
abcde8e6ce
commit
77476d5cbf
6 changed files with 200 additions and 0 deletions
|
|
@ -45,6 +45,8 @@ import {
|
|||
inferSourceGroundingHint,
|
||||
renderRateLimitMessage,
|
||||
renderSystemMessage,
|
||||
renderInteractionProfileDisclosure,
|
||||
sendDirectMessage,
|
||||
sendChunkedChannelMessage,
|
||||
splitDiscordContent,
|
||||
truncate,
|
||||
|
|
@ -98,6 +100,9 @@ export const commandDefinitions = [
|
|||
new SlashCommandBuilder()
|
||||
.setName("provider-status")
|
||||
.setDescription("Show current provider availability for you."),
|
||||
new SlashCommandBuilder()
|
||||
.setName("profile")
|
||||
.setDescription("DM your current Void profile read to you."),
|
||||
new SlashCommandBuilder()
|
||||
.setName("search-history")
|
||||
.setDescription("Search indexed channel history.")
|
||||
|
|
@ -232,6 +237,7 @@ export async function handlePrompt(options: PromptHandlerOptions): Promise<void>
|
|||
situationalSocialRead.pronounEvidence,
|
||||
)) ?? interactionMemory;
|
||||
}
|
||||
|
||||
const sourceGrounding = inferSourceGroundingHint(
|
||||
options.prompt,
|
||||
await options.sourceArchiveRepository.listRepoSummaries(),
|
||||
|
|
@ -502,6 +508,64 @@ export async function handleSummarizeChannel(
|
|||
);
|
||||
}
|
||||
|
||||
export async function handleProfile(options: {
|
||||
interaction: ChatInputCommandInteraction<CacheType>;
|
||||
actor: Actor;
|
||||
interactionMemory: InteractionMemoryBank;
|
||||
auditLog: AuditLog;
|
||||
systemMessages: SystemMessageCatalog;
|
||||
}): Promise<void> {
|
||||
const profile = await options.interactionMemory.getProfile(options.actor.id);
|
||||
const disclosure = renderInteractionProfileDisclosure(
|
||||
options.actor.displayName,
|
||||
profile,
|
||||
);
|
||||
|
||||
if (!options.interaction.inGuild()) {
|
||||
await replyEphemeral(options.interaction, disclosure);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendDirectMessage(options.interaction.user, disclosure);
|
||||
await options.auditLog.record({
|
||||
type: "profile.dm_sent",
|
||||
actorId: options.actor.id,
|
||||
details: {
|
||||
command: "profile",
|
||||
channelId: options.interaction.channelId,
|
||||
channelName:
|
||||
options.interaction.channel && "name" in options.interaction.channel
|
||||
? options.interaction.channel.name ?? null
|
||||
: null,
|
||||
},
|
||||
});
|
||||
await replyEphemeral(
|
||||
options.interaction,
|
||||
renderSystemMessage(options.systemMessages, "profile.dm_sent"),
|
||||
);
|
||||
} catch (error) {
|
||||
await options.auditLog.record({
|
||||
type: "profile.dm_failed",
|
||||
actorId: options.actor.id,
|
||||
details: {
|
||||
command: "profile",
|
||||
channelId: options.interaction.channelId,
|
||||
channelName:
|
||||
options.interaction.channel && "name" in options.interaction.channel
|
||||
? options.interaction.channel.name ?? null
|
||||
: null,
|
||||
errorMessage:
|
||||
error instanceof Error ? error.message : "Unexpected DM delivery failure.",
|
||||
},
|
||||
});
|
||||
await replyEphemeral(
|
||||
options.interaction,
|
||||
renderSystemMessage(options.systemMessages, "profile.dm_failed"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleReindexChannel(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
actor: Actor,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import {
|
|||
type ChannelIndexingPolicy,
|
||||
type ChannelIndexingTarget,
|
||||
type GuildContext,
|
||||
type InteractionMemoryProfile,
|
||||
type SourceGroundingHint,
|
||||
type SourceMessage,
|
||||
type SituationalSocialRead,
|
||||
shouldIndexChannel,
|
||||
} from "@voidbot/shared";
|
||||
|
||||
|
|
@ -408,6 +410,120 @@ export async function notifyOwnerOfBotIssue(
|
|||
}
|
||||
}
|
||||
|
||||
export async function sendDirectMessage(
|
||||
target: { send: (content: string) => Promise<unknown> },
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
for (const chunk of splitDiscordContent(content)) {
|
||||
await target.send(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderInteractionProfileDisclosure(
|
||||
actorName: string,
|
||||
profile: InteractionMemoryProfile | undefined,
|
||||
situationalSocialRead?: SituationalSocialRead,
|
||||
): string {
|
||||
if (!profile) {
|
||||
return [
|
||||
`Void's current read on ${actorName}:`,
|
||||
"",
|
||||
"There is not enough durable interaction memory yet to form a meaningful long-horizon profile.",
|
||||
situationalSocialRead
|
||||
? [
|
||||
"",
|
||||
"Ephemeral room read for this moment:",
|
||||
`- Summary: ${situationalSocialRead.summary}`,
|
||||
`- Room tone: ${situationalSocialRead.roomTone}`,
|
||||
`- Speaker read: ${situationalSocialRead.speakerCurrentRead}`,
|
||||
`- Social frame: ${situationalSocialRead.socialFrame}`,
|
||||
`- Response guidance: ${situationalSocialRead.responseGuidance}`,
|
||||
].join("\n")
|
||||
: "",
|
||||
]
|
||||
.filter((section) => section.length > 0)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const dimensions =
|
||||
profile.interactionDimensions.length > 0
|
||||
? profile.interactionDimensions
|
||||
.map(
|
||||
(dimension) =>
|
||||
`- ${dimension.label} (${dimension.score}/3): ${dimension.summary}`,
|
||||
)
|
||||
.join("\n")
|
||||
: "- No strong interaction dimensions yet.";
|
||||
const traits =
|
||||
profile.inferredTraits.length > 0
|
||||
? profile.inferredTraits.join(", ")
|
||||
: "No stable inferred traits yet.";
|
||||
const recentEvents =
|
||||
profile.recentEvents.length > 0
|
||||
? profile.recentEvents
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(event) =>
|
||||
`- [${event.timestamp}] ${event.sourceKind === "ambient_mention" ? "ambient" : "direct"} ${event.sentiment} score=${event.score}: ${event.summary} Quote: "${event.excerpt}"`,
|
||||
)
|
||||
.join("\n")
|
||||
: "- No retained recent incidents.";
|
||||
const pronounEvidence =
|
||||
profile.pronounEvidence.length > 0
|
||||
? profile.pronounEvidence
|
||||
.map(
|
||||
(entry) =>
|
||||
`- [${entry.timestamp}] ${entry.stance} ${entry.pronounSet} via ${entry.source} (${Math.round(entry.confidence * 100)}%): "${entry.excerpt}"`,
|
||||
)
|
||||
.join("\n")
|
||||
: "- No stored pronoun evidence yet.";
|
||||
const situationalSection = situationalSocialRead
|
||||
? [
|
||||
"",
|
||||
"Ephemeral room read for this moment:",
|
||||
`- Summary: ${situationalSocialRead.summary}`,
|
||||
`- Room tone: ${situationalSocialRead.roomTone}`,
|
||||
`- Speaker read: ${situationalSocialRead.speakerCurrentRead}`,
|
||||
`- Social frame: ${situationalSocialRead.socialFrame}`,
|
||||
`- Response guidance: ${situationalSocialRead.responseGuidance}`,
|
||||
].join("\n")
|
||||
: "";
|
||||
|
||||
return [
|
||||
`Void's current profile on ${actorName}:`,
|
||||
"",
|
||||
`Summary: ${profile.summary}`,
|
||||
`Disposition: ${profile.disposition}`,
|
||||
`Affinity score: ${profile.affinityScore}`,
|
||||
`Interaction counts: total=${profile.totalInteractions}, direct=${profile.directInteractionCount}, ambient=${profile.ambientMentionCount}, positive=${profile.positiveCount}, neutral=${profile.neutralCount}, negative=${profile.negativeCount}`,
|
||||
`Last interaction: ${profile.lastInteractionAt ?? "unknown"}`,
|
||||
"",
|
||||
`Psychological read: ${profile.psychologicalProfile}`,
|
||||
`Inferred traits: ${traits}`,
|
||||
"",
|
||||
"Interaction dimensions:",
|
||||
dimensions,
|
||||
"",
|
||||
"Pronoun handling:",
|
||||
`- Policy: ${profile.pronounPolicy}`,
|
||||
`- Resolved set: ${profile.resolvedPronounSet ?? "none"}`,
|
||||
`- Confidence: ${profile.pronounConfidence !== undefined ? `${Math.round(profile.pronounConfidence * 100)}%` : "n/a"}`,
|
||||
`- Guidance: ${profile.pronounGuidance}`,
|
||||
"- Evidence:",
|
||||
pronounEvidence,
|
||||
"",
|
||||
`Private response guidance: ${profile.responseGuidance}`,
|
||||
"",
|
||||
"Recent remembered incidents:",
|
||||
recentEvents,
|
||||
situationalSection,
|
||||
]
|
||||
.filter((section) => section.length > 0)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function truncate(input: string, limit: number): string {
|
||||
if (input.length <= limit) {
|
||||
return input;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { shouldIndexChannel } from "@voidbot/shared";
|
|||
|
||||
import {
|
||||
handleApproveJob,
|
||||
handleProfile,
|
||||
handlePrompt,
|
||||
handleRejectJob,
|
||||
handleReindexChannel,
|
||||
|
|
@ -516,6 +517,15 @@ export async function startBot(): Promise<void> {
|
|||
),
|
||||
);
|
||||
break;
|
||||
case "profile":
|
||||
await handleProfile({
|
||||
interaction,
|
||||
actor,
|
||||
interactionMemory,
|
||||
auditLog,
|
||||
systemMessages: activeSystemMessages,
|
||||
});
|
||||
break;
|
||||
case "search-history":
|
||||
await handleSearchHistory(interaction, retrievalService, activeSystemMessages);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -139,6 +139,14 @@
|
|||
"Current provider status:",
|
||||
"Here is the current state of the configured providers:"
|
||||
],
|
||||
"profile.dm_sent": [
|
||||
"I sent your current profile read by DM.",
|
||||
"Your profile read is in your DMs."
|
||||
],
|
||||
"profile.dm_failed": [
|
||||
"I tried to DM your profile, but Discord would not let me through.",
|
||||
"I could not deliver the profile DM. Your privacy settings or DM permissions may be in the way."
|
||||
],
|
||||
"owner_codex.handoff_notice": [
|
||||
"This request needs the fuller Codex flow. Check `{handoffPath}` and `{debugPath}` in the local workspace.",
|
||||
"This request is leaving Discord. Continue from `{handoffPath}` and use `{debugPath}` for the trace."
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const commandNames = [
|
|||
"approve-job",
|
||||
"reject-job",
|
||||
"provider-status",
|
||||
"profile",
|
||||
"reindex-channel",
|
||||
"set-style",
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -26,3 +26,4 @@
|
|||
{"ts":"2026-05-02T20:25:00+01:00","type":"architecture","status":"ok","note":"Ephemeral room-reading now comes from a separate `think=false` local Ollama sidecar pass over recent room context instead of hand-built phrase heuristics, and the inferred reads are stored as audit events so they can later be aggregated into richer profiles without immediately hardening every passing vibe into durable identity truth."}
|
||||
{"ts":"2026-05-02T21:05:00+01:00","type":"bugfix","status":"ok","note":"Archived-history answers should not make the model infer time sense from bare ISO strings alone; history tool results now carry human-readable time context, and both reply lanes are explicitly told to narrate old events in the correct tense and avoid guessing pronouns when the supplied context does not state them."}
|
||||
{"ts":"2026-05-02T21:13:34+01:00","type":"architecture","status":"ok","note":"Pronoun handling now runs on structured evidence rather than name-guessing: the room-read sidecar emits current-speaker pronoun cues with provenance and confidence, interaction memory persists and merges them as identity state, and explicit self-statements or corrections override softer contextual inferences before the reply lanes consume private pronoun guidance."}
|
||||
{"ts":"2026-05-02T21:23:00+01:00","type":"implementation","status":"ok","note":"Profile disclosure is now a dedicated `/profile` slash command instead of fuzzy chat intent detection: it DMs the caller a fuller internal profile read, keeps the public channel to a short confirmation or failure notice, and avoids turning privacy-sensitive introspection into another free-form language parsing stunt."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue