feat: add memory selection metrics to HypaV3 (#861)

# PR Checklist
- [ ] Have you checked if it works normally in all models? *Ignore this
if it doesn't use models.*
- [ ] Have you checked if it works normally in all web, local, and node
hosted versions? If it doesn't, have you blocked it in those versions?
- [ ] Have you added type definitions?

# Preview

![preview](https://github.com/user-attachments/assets/8b805ff5-6242-401b-a1c2-ca3ca17355d6)

# Description
This PR introduces following:
- Add memory selection metrics to HypaV3
This commit is contained in:
kwaroran
2025-05-24 20:35:47 +09:00
committed by GitHub
2 changed files with 207 additions and 87 deletions

View File

@@ -3,6 +3,8 @@
import { import {
SearchIcon, SearchIcon,
SettingsIcon, SettingsIcon,
MoreVerticalIcon,
BarChartIcon,
Trash2Icon, Trash2Icon,
XIcon, XIcon,
ChevronUpIcon, ChevronUpIcon,
@@ -88,6 +90,8 @@
let expandedMessageUIState = $state<ExpandedMessageUI>(null); let expandedMessageUIState = $state<ExpandedMessageUI>(null);
let searchUIState = $state<SearchUI>(null); let searchUIState = $state<SearchUI>(null);
let showImportantOnly = $state(false); let showImportantOnly = $state(false);
let showDropdown = $state(false);
let showMetrics = $state(false);
$effect.pre(() => { $effect.pre(() => {
untrack(() => { untrack(() => {
@@ -95,7 +99,6 @@
DBState.db.characters[$selectedCharID].chatPage DBState.db.characters[$selectedCharID].chatPage
].hypaV3Data ??= { ].hypaV3Data ??= {
summaries: [], summaries: [],
lastSelectedSummaries: [],
}; };
}); });
@@ -187,15 +190,14 @@
// Search summary index // Search summary index
if (query.match(/^#\d+$/)) { if (query.match(/^#\d+$/)) {
const summaryNumber = parseInt(query.substring(1)) - 1; const summaryIndex = parseInt(query.substring(1)) - 1;
if ( if (
summaryNumber >= 0 && summaryIndex >= 0 &&
summaryNumber < hypaV3DataState.summaries.length && summaryIndex < hypaV3DataState.summaries.length &&
(!showImportantOnly || isSummaryVisible(summaryIndex)
hypaV3DataState.summaries[summaryNumber].isImportant)
) { ) {
results.push(new SummarySearchResult(summaryNumber, 0, 0)); results.push(new SummarySearchResult(summaryIndex, 0, 0));
} }
return results; return results;
@@ -204,10 +206,7 @@
if (isGuidLike(query)) { if (isGuidLike(query)) {
// Search chatMemo // Search chatMemo
summaryUIStates.forEach((summaryUI, summaryIndex) => { summaryUIStates.forEach((summaryUI, summaryIndex) => {
if ( if (isSummaryVisible(summaryIndex)) {
!showImportantOnly ||
hypaV3DataState.summaries[summaryIndex].isImportant
) {
summaryUI.chatMemoRefs.forEach((buttonRef, memoIndex) => { summaryUI.chatMemoRefs.forEach((buttonRef, memoIndex) => {
const buttonText = buttonRef.textContent?.toLowerCase() || ""; const buttonText = buttonRef.textContent?.toLowerCase() || "";
@@ -220,10 +219,7 @@
} else { } else {
// Search summary // Search summary
summaryUIStates.forEach((summaryUI, summaryIndex) => { summaryUIStates.forEach((summaryUI, summaryIndex) => {
if ( if (isSummaryVisible(summaryIndex)) {
!showImportantOnly ||
hypaV3DataState.summaries[summaryIndex].isImportant
) {
const textAreaText = summaryUI.originalRef.value?.toLowerCase(); const textAreaText = summaryUI.originalRef.value?.toLowerCase();
let pos = -1; let pos = -1;
@@ -375,6 +371,23 @@
textarea.scrollTop = selectionTop - textarea.clientHeight / 2; textarea.scrollTop = selectionTop - textarea.clientHeight / 2;
} }
function isSummaryVisible(index: number): boolean {
const summary = hypaV3DataState.summaries[index];
const metrics = hypaV3DataState.metrics;
const metricsFilter =
!showMetrics ||
!metrics ||
metrics.lastImportantSummaries.includes(index) ||
metrics.lastRecentSummaries.includes(index) ||
metrics.lastSimilarSummaries.includes(index) ||
metrics.lastRandomSummaries.includes(index);
const importantFilter = !showImportantOnly || summary.isImportant;
return metricsFilter && importantFilter;
}
async function toggleTranslate( async function toggleTranslate(
summaryIndex: number, summaryIndex: number,
regenerate?: boolean regenerate?: boolean
@@ -730,7 +743,6 @@
chatMemos: [...mainChunk.chatMemos], chatMemos: [...mainChunk.chatMemos],
isImportant: false, isImportant: false,
})), })),
lastSelectedSummaries: [],
}; };
chat.hypaV3Data = newHypaV3Data; chat.hypaV3Data = newHypaV3Data;
@@ -819,11 +831,17 @@
<!-- Modal wrapper --> <!-- Modal wrapper -->
<div class="flex justify-center w-full h-full"> <div class="flex justify-center w-full h-full">
<!-- Modal window --> <!-- Modal window -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="flex flex-col p-3 sm:p-6 rounded-lg bg-zinc-900 w-full max-w-3xl {hypaV3DataState class="flex flex-col p-3 sm:p-6 rounded-lg bg-zinc-900 w-full max-w-3xl {hypaV3DataState
.summaries.length === 0 .summaries.length === 0
? 'h-fit' ? 'h-fit'
: 'h-full'}" : 'h-full'}"
onclick={(e) => {
e.stopPropagation();
showDropdown = false;
}}
> >
<!-- Header --> <!-- Header -->
<div class="flex justify-between items-center mb-2 sm:mb-4"> <div class="flex justify-between items-center mb-2 sm:mb-4">
@@ -831,6 +849,7 @@
<h1 class="text-lg sm:text-2xl font-semibold text-zinc-300"> <h1 class="text-lg sm:text-2xl font-semibold text-zinc-300">
{language.hypaV3Modal.titleLabel} {language.hypaV3Modal.titleLabel}
</h1> </h1>
<!-- Buttons Container --> <!-- Buttons Container -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Search Button --> <!-- Search Button -->
@@ -874,6 +893,44 @@
<SettingsIcon class="w-6 h-6" /> <SettingsIcon class="w-6 h-6" />
</button> </button>
<!-- Show Dropdown Button -->
<div class="relative">
<button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
tabindex="-1"
onclick={(e) => {
e.stopPropagation();
showDropdown = true;
}}
>
<MoreVerticalIcon class="w-6 h-6" />
</button>
{#if showDropdown}
<div
class="absolute z-10 right-0 mt-1 p-2 rounded-md shadow-lg border border-zinc-700 bg-zinc-800"
>
<!-- Buttons Container -->
<div class="flex items-center gap-2">
<!-- Show Metrics Button -->
<button
class="p-2 transition-colors {showMetrics
? 'text-blue-400 hover:text-blue-300'
: 'text-zinc-400 hover:text-zinc-200'}"
tabindex="-1"
onclick={() => {
if (searchUIState) {
searchUIState.query = "";
searchUIState.results = [];
searchUIState.currentResultIndex = -1;
}
showMetrics = !showMetrics;
}}
>
<BarChartIcon class="w-6 h-6" />
</button>
<!-- Reset Button --> <!-- Reset Button -->
<button <button
class="p-2 text-zinc-400 hover:text-rose-300 transition-colors" class="p-2 text-zinc-400 hover:text-rose-300 transition-colors"
@@ -889,13 +946,16 @@
DBState.db.characters[$selectedCharID].chatPage DBState.db.characters[$selectedCharID].chatPage
].hypaV3Data = { ].hypaV3Data = {
summaries: [], summaries: [],
lastSelectedSummaries: [],
}; };
} }
}} }}
> >
<Trash2Icon class="w-6 h-6" /> <Trash2Icon class="w-6 h-6" />
</button> </button>
</div>
</div>
{/if}
</div>
<!-- Close Button --> <!-- Close Button -->
<button <button
@@ -954,7 +1014,7 @@
<!-- Search Bar --> <!-- Search Bar -->
{:else if searchUIState} {:else if searchUIState}
<div class="sticky top-0 z-40 p-2 sm:p-3 bg-zinc-800"> <div class="sticky top-0 p-2 sm:p-3 bg-zinc-800">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="relative flex flex-1 items-center"> <div class="relative flex flex-1 items-center">
<form <form
@@ -1016,7 +1076,7 @@
<!-- Summaries List --> <!-- Summaries List -->
{#each hypaV3DataState.summaries as summary, i} {#each hypaV3DataState.summaries as summary, i}
{#if !showImportantOnly || summary.isImportant} {#if isSummaryVisible(i)}
{#if summaryUIStates[i]} {#if summaryUIStates[i]}
<!-- Summary Item --> <!-- Summary Item -->
<div <div
@@ -1024,6 +1084,8 @@
> >
<!-- Original Summary Header --> <!-- Original Summary Header -->
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<!-- Summary Number / Metrics Container -->
<div class="flex items-center gap-2">
<span class="text-sm text-zinc-400" <span class="text-sm text-zinc-400"
>{language.hypaV3Modal.summaryNumberLabel.replace( >{language.hypaV3Modal.summaryNumberLabel.replace(
"{0}", "{0}",
@@ -1031,6 +1093,41 @@
)}</span )}</span
> >
{#if showMetrics && hypaV3DataState.metrics}
<div class="flex flex-wrap gap-1">
{#if hypaV3DataState.metrics.lastImportantSummaries.includes(i)}
<span
class="px-1.5 py-0.5 rounded-full text-xs whitespace-nowrap text-purple-200 bg-purple-900/70"
>
Important
</span>
{/if}
{#if hypaV3DataState.metrics.lastRecentSummaries.includes(i)}
<span
class="px-1.5 py-0.5 rounded-full text-xs whitespace-nowrap text-blue-200 bg-blue-900/70"
>
Recent
</span>
{/if}
{#if hypaV3DataState.metrics.lastSimilarSummaries.includes(i)}
<span
class="px-1.5 py-0.5 rounded-full text-xs whitespace-nowrap text-green-200 bg-green-900/70"
>
Similar
</span>
{/if}
{#if hypaV3DataState.metrics.lastRandomSummaries.includes(i)}
<span
class="px-1.5 py-0.5 rounded-full text-xs whitespace-nowrap text-yellow-200 bg-yellow-900/70"
>
Random
</span>
{/if}
</div>
{/if}
</div>
<!-- Buttons Container -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Translate Button --> <!-- Translate Button -->
<button <button

View File

@@ -41,7 +41,13 @@ export interface HypaV3Settings {
interface HypaV3Data { interface HypaV3Data {
summaries: Summary[]; summaries: Summary[];
lastSelectedSummaries?: number[]; lastSelectedSummaries?: number[]; // legacy
metrics?: {
lastImportantSummaries: number[];
lastRecentSummaries: number[];
lastSimilarSummaries: number[];
lastRandomSummaries: number[];
};
} }
export interface SerializableHypaV3Data { export interface SerializableHypaV3Data {
@@ -50,7 +56,13 @@ export interface SerializableHypaV3Data {
chatMemos: string[]; chatMemos: string[];
isImportant: boolean; isImportant: boolean;
}[]; }[];
lastSelectedSummaries?: number[]; lastSelectedSummaries?: number[]; // legacy
metrics?: {
lastImportantSummaries: number[];
lastRecentSummaries: number[];
lastSimilarSummaries: number[];
lastRandomSummaries: number[];
};
} }
interface Summary { interface Summary {
@@ -158,15 +170,12 @@ async function hypaMemoryV3MainExp(
currentTokens -= db.maxResponse; currentTokens -= db.maxResponse;
// Load existing hypa data if available // Load existing hypa data if available
let data: HypaV3Data = { const data: HypaV3Data = room.hypaV3Data
? toHypaV3Data(room.hypaV3Data)
: {
summaries: [], summaries: [],
lastSelectedSummaries: [],
}; };
if (room.hypaV3Data) {
data = toHypaV3Data(room.hypaV3Data);
}
// Clean orphaned summaries // Clean orphaned summaries
if (!settings.preserveOrphanedMemory) { if (!settings.preserveOrphanedMemory) {
cleanOrphanedSummary(chats, data); cleanOrphanedSummary(chats, data);
@@ -464,11 +473,10 @@ async function hypaMemoryV3MainExp(
const selectedSummaries: Summary[] = []; const selectedSummaries: Summary[] = [];
const randomMemoryRatio = const randomMemoryRatio =
1 - settings.recentMemoryRatio - settings.similarMemoryRatio; 1 - settings.recentMemoryRatio - settings.similarMemoryRatio;
const selectedImportantSummaries: Summary[] = [];
// Select important summaries // Select important summaries
{ {
const selectedImportantSummaries: Summary[] = [];
for (const summary of data.summaries) { for (const summary of data.summaries) {
if (summary.isImportant) { if (summary.isImportant) {
const summaryTokens = await tokenizer.tokenizeChat({ const summaryTokens = await tokenizer.tokenizeChat({
@@ -505,10 +513,9 @@ async function hypaMemoryV3MainExp(
availableMemoryTokens * settings.recentMemoryRatio availableMemoryTokens * settings.recentMemoryRatio
); );
let consumedRecentMemoryTokens = 0; let consumedRecentMemoryTokens = 0;
if (settings.recentMemoryRatio > 0) {
const selectedRecentSummaries: Summary[] = []; const selectedRecentSummaries: Summary[] = [];
if (settings.recentMemoryRatio > 0) {
// Target only summaries that haven't been selected yet // Target only summaries that haven't been selected yet
const unusedSummaries = data.summaries.filter( const unusedSummaries = data.summaries.filter(
(e) => !selectedSummaries.includes(e) (e) => !selectedSummaries.includes(e)
@@ -554,10 +561,9 @@ async function hypaMemoryV3MainExp(
availableMemoryTokens * settings.similarMemoryRatio availableMemoryTokens * settings.similarMemoryRatio
); );
let consumedSimilarMemoryTokens = 0; let consumedSimilarMemoryTokens = 0;
if (settings.similarMemoryRatio > 0) {
const selectedSimilarSummaries: Summary[] = []; const selectedSimilarSummaries: Summary[] = [];
if (settings.similarMemoryRatio > 0) {
// Utilize unused token space from recent selection // Utilize unused token space from recent selection
if (randomMemoryRatio <= 0) { if (randomMemoryRatio <= 0) {
const unusedRecentTokens = const unusedRecentTokens =
@@ -769,10 +775,9 @@ async function hypaMemoryV3MainExp(
availableMemoryTokens * randomMemoryRatio availableMemoryTokens * randomMemoryRatio
); );
let consumedRandomMemoryTokens = 0; let consumedRandomMemoryTokens = 0;
if (randomMemoryRatio > 0) {
const selectedRandomSummaries: Summary[] = []; const selectedRandomSummaries: Summary[] = [];
if (randomMemoryRatio > 0) {
// Utilize unused token space from recent and similar selection // Utilize unused token space from recent and similar selection
const unusedRecentTokens = const unusedRecentTokens =
reservedRecentMemoryTokens - consumedRecentMemoryTokens; reservedRecentMemoryTokens - consumedRecentMemoryTokens;
@@ -872,9 +877,20 @@ async function hypaMemoryV3MainExp(
} }
// Save last selected summaries // Save last selected summaries
data.lastSelectedSummaries = selectedSummaries.map((selectedSummary) => data.metrics = {
data.summaries.findIndex((summary) => summary === selectedSummary) lastImportantSummaries: selectedImportantSummaries.map((selected) =>
); data.summaries.findIndex((sum) => sum === selected)
),
lastRecentSummaries: selectedRecentSummaries.map((selected) =>
data.summaries.findIndex((sum) => sum === selected)
),
lastSimilarSummaries: selectedSimilarSummaries.map((selected) =>
data.summaries.findIndex((sum) => sum === selected)
),
lastRandomSummaries: selectedRandomSummaries.map((selected) =>
data.summaries.findIndex((sum) => sum === selected)
),
};
const newChats: OpenAIChat[] = [ const newChats: OpenAIChat[] = [
{ {
@@ -927,15 +943,12 @@ async function hypaMemoryV3Main(
currentTokens -= db.maxResponse; currentTokens -= db.maxResponse;
// Load existing hypa data if available // Load existing hypa data if available
let data: HypaV3Data = { const data: HypaV3Data = room.hypaV3Data
? toHypaV3Data(room.hypaV3Data)
: {
summaries: [], summaries: [],
lastSelectedSummaries: [],
}; };
if (room.hypaV3Data) {
data = toHypaV3Data(room.hypaV3Data);
}
// Clean orphaned summaries // Clean orphaned summaries
if (!settings.preserveOrphanedMemory) { if (!settings.preserveOrphanedMemory) {
cleanOrphanedSummary(chats, data); cleanOrphanedSummary(chats, data);
@@ -1171,11 +1184,10 @@ async function hypaMemoryV3Main(
const selectedSummaries: Summary[] = []; const selectedSummaries: Summary[] = [];
const randomMemoryRatio = const randomMemoryRatio =
1 - settings.recentMemoryRatio - settings.similarMemoryRatio; 1 - settings.recentMemoryRatio - settings.similarMemoryRatio;
const selectedImportantSummaries: Summary[] = [];
// Select important summaries // Select important summaries
{ {
const selectedImportantSummaries: Summary[] = [];
for (const summary of data.summaries) { for (const summary of data.summaries) {
if (summary.isImportant) { if (summary.isImportant) {
const summaryTokens = await tokenizer.tokenizeChat({ const summaryTokens = await tokenizer.tokenizeChat({
@@ -1212,10 +1224,9 @@ async function hypaMemoryV3Main(
availableMemoryTokens * settings.recentMemoryRatio availableMemoryTokens * settings.recentMemoryRatio
); );
let consumedRecentMemoryTokens = 0; let consumedRecentMemoryTokens = 0;
if (settings.recentMemoryRatio > 0) {
const selectedRecentSummaries: Summary[] = []; const selectedRecentSummaries: Summary[] = [];
if (settings.recentMemoryRatio > 0) {
// Target only summaries that haven't been selected yet // Target only summaries that haven't been selected yet
const unusedSummaries = data.summaries.filter( const unusedSummaries = data.summaries.filter(
(e) => !selectedSummaries.includes(e) (e) => !selectedSummaries.includes(e)
@@ -1261,10 +1272,9 @@ async function hypaMemoryV3Main(
availableMemoryTokens * settings.similarMemoryRatio availableMemoryTokens * settings.similarMemoryRatio
); );
let consumedSimilarMemoryTokens = 0; let consumedSimilarMemoryTokens = 0;
if (settings.similarMemoryRatio > 0) {
const selectedSimilarSummaries: Summary[] = []; const selectedSimilarSummaries: Summary[] = [];
if (settings.similarMemoryRatio > 0) {
// Utilize unused token space from recent selection // Utilize unused token space from recent selection
if (randomMemoryRatio <= 0) { if (randomMemoryRatio <= 0) {
const unusedRecentTokens = const unusedRecentTokens =
@@ -1441,10 +1451,9 @@ async function hypaMemoryV3Main(
availableMemoryTokens * randomMemoryRatio availableMemoryTokens * randomMemoryRatio
); );
let consumedRandomMemoryTokens = 0; let consumedRandomMemoryTokens = 0;
if (randomMemoryRatio > 0) {
const selectedRandomSummaries: Summary[] = []; const selectedRandomSummaries: Summary[] = [];
if (randomMemoryRatio > 0) {
// Utilize unused token space from recent and similar selection // Utilize unused token space from recent and similar selection
const unusedRecentTokens = const unusedRecentTokens =
reservedRecentMemoryTokens - consumedRecentMemoryTokens; reservedRecentMemoryTokens - consumedRecentMemoryTokens;
@@ -1546,9 +1555,20 @@ async function hypaMemoryV3Main(
} }
// Save last selected summaries // Save last selected summaries
data.lastSelectedSummaries = selectedSummaries.map((selectedSummary) => data.metrics = {
data.summaries.findIndex((summary) => summary === selectedSummary) lastImportantSummaries: selectedImportantSummaries.map((selected) =>
); data.summaries.findIndex((sum) => sum === selected)
),
lastRecentSummaries: selectedRecentSummaries.map((selected) =>
data.summaries.findIndex((sum) => sum === selected)
),
lastSimilarSummaries: selectedSimilarSummaries.map((selected) =>
data.summaries.findIndex((sum) => sum === selected)
),
lastRandomSummaries: selectedRandomSummaries.map((selected) =>
data.summaries.findIndex((sum) => sum === selected)
),
};
const newChats: OpenAIChat[] = [ const newChats: OpenAIChat[] = [
{ {
@@ -1578,8 +1598,11 @@ async function hypaMemoryV3Main(
} }
function toHypaV3Data(serialData: SerializableHypaV3Data): HypaV3Data { function toHypaV3Data(serialData: SerializableHypaV3Data): HypaV3Data {
// Remove legacy property
const { lastSelectedSummaries, ...restData } = serialData;
return { return {
...serialData, ...restData,
summaries: serialData.summaries.map((summary) => ({ summaries: serialData.summaries.map((summary) => ({
...summary, ...summary,
// Convert null back to undefined (JSON serialization converts undefined to null) // Convert null back to undefined (JSON serialization converts undefined to null)