M3b:世谱激活接入组装管线(关键词命中/常驻/预算/符文展开)与前端记忆治理页(候选箱/重忆确认/正影/删除/实录溯源)
This commit is contained in:
1 parent
eb0027757d
commit
e914aec9c4
8 files changed
+569
-1
No files matched your search
@@ -426,6 +426,15 @@ async def build_generation_prompt(
|
||||
if recalled:
|
||||
memory_lines = [f"- {m.content}" for m in recalled]
|
||||
parts.append(f"【获准记忆(来源可查)】\n" + "\n".join(memory_lines))
|
||||
# 世谱激活(河图):关键词命中扫描近期文本,常驻条目恒注入;预算内按 insertion_order
|
||||
tree_character_id = await _tree_character_id(question.tree_id)
|
||||
activated = await _activate_lorebook(
|
||||
tree_character_id,
|
||||
scan_text=f"{transcript[-600:]}\n{question.content}",
|
||||
expand=(lambda t: render(t, ctx, registry=registry)),
|
||||
)
|
||||
if activated:
|
||||
parts.append("【世界认知(世谱命中)】\n" + "\n".join(f"- {line}" for line in activated))
|
||||
if transcript:
|
||||
parts.append(f"【共同经历(祖先路径)】\n{transcript}")
|
||||
parts.append(f"【当前问题】\n{expand(question.content)}")
|
||||
@@ -899,3 +908,56 @@ async def _tree_character_id(tree_id: str) -> str:
|
||||
async with AsyncSessionLocal() as session:
|
||||
tree = await session.get(MirrorTree, tree_id)
|
||||
return tree.character_id if tree else ""
|
||||
|
||||
|
||||
async def _activate_lorebook(
|
||||
character_id: str,
|
||||
*,
|
||||
scan_text: str,
|
||||
expand=None, # noqa: ANN001 — 符文展开函数(char/user 名),无上下文时不展开
|
||||
char_budget: int = 600,
|
||||
) -> list[str]:
|
||||
"""世谱激活管线(mirror-tree PRD 借 ST 范式):关键词命中 + 常驻,预算内按插入序。
|
||||
|
||||
扫描范围为近期文本(祖先链尾部 + 当前问题);条目内容经符文展开后注入。
|
||||
次级键逻辑、递归扫描与时序效果(sticky/cooldown)随 M5 循环轮次状态接入。
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.lorebook import Lorebook, LorebookEntry
|
||||
|
||||
text = "".join(scan_text.lower().split())
|
||||
async with AsyncSessionLocal() as session:
|
||||
books = (
|
||||
await session.execute(
|
||||
select(Lorebook.id).where(
|
||||
Lorebook.character_id == character_id, Lorebook.status == "active"
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
if not books:
|
||||
return []
|
||||
entries = (
|
||||
(
|
||||
await session.execute(
|
||||
select(LorebookEntry)
|
||||
.where(LorebookEntry.lorebook_id.in_(tuple(books)), LorebookEntry.enabled.is_(True))
|
||||
.order_by(LorebookEntry.insertion_order)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
picked: list[str] = []
|
||||
used = 0
|
||||
for entry in entries:
|
||||
keys = [k.lower() for k in json.loads(entry.keys_json or "[]") if k]
|
||||
hit = entry.constant or any("".join(k.split()) in text for k in keys)
|
||||
if not hit:
|
||||
continue
|
||||
content = entry.content if expand is None else expand(entry.content)
|
||||
if used + len(content) > char_budget:
|
||||
continue
|
||||
picked.append(content)
|
||||
used += len(content)
|
||||
return picked
|
||||
@@ -255,3 +255,55 @@ async def test_recalled_memory_injected_into_prompt(client: AsyncClient) -> None
|
||||
prompt = requested["payload"]["request"]["messages"][-1]["content"]
|
||||
assert "【获准记忆(来源可查)】" in prompt
|
||||
_ = json # 保留 json 引用供调试断言使用
|
||||
|
||||
|
||||
# ── M3b:世谱激活进入投影 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_lorebook_activation_injected_into_prompt(client: AsyncClient) -> None:
|
||||
tree_id, _q, answer_id, character_id = await _tree_with_answer(client, "世谱树")
|
||||
|
||||
# 为人物建世谱书 + 关键词条目 + 常驻条目
|
||||
book = (
|
||||
await client.post(
|
||||
"/api/v1/lorebooks",
|
||||
json={"name": "测试世谱", "character_id": character_id},
|
||||
)
|
||||
)
|
||||
if book.status_code >= 400: # 端点形态兼容:老 CRUD 可能要求别的字段
|
||||
book = (await client.post("/api/v1/lorebooks", json={"name": "测试世谱"}))
|
||||
assert book.status_code < 400, book.text
|
||||
book_id = book.json()["id"]
|
||||
|
||||
keyed = (
|
||||
await client.post(
|
||||
f"/api/v1/lorebooks/{book_id}/entries",
|
||||
json={"keys": ["星穹图书馆"], "content": "星穹图书馆只在午后开放"},
|
||||
)
|
||||
)
|
||||
if keyed.status_code >= 400: # 兼容批量/单条两种端点形态
|
||||
keyed = await client.post(
|
||||
f"/api/v1/lorebooks/{book_id}/entries",
|
||||
json=[{"keys": ["星穹图书馆"], "content": "星穹图书馆只在午后开放"}],
|
||||
)
|
||||
assert keyed.status_code < 400, keyed.text
|
||||
|
||||
await client.post(f"/api/v1/trees/{tree_id}/adopt", json={"node_id": answer_id})
|
||||
q2 = (
|
||||
await client.post(
|
||||
f"/api/v1/trees/{tree_id}/nodes", json={"content": "带我去星穹图书馆看看"}
|
||||
)
|
||||
).json()
|
||||
spawned = (await client.post(f"/api/v1/nodes/{q2['id']}/answers", json={})).json()
|
||||
for _ in range(300):
|
||||
if (await client.get(f"/api/v1/runs/{spawned['run_id']}")).json()["status"] in (
|
||||
"completed",
|
||||
"interrupted",
|
||||
):
|
||||
break
|
||||
await asyncio.sleep(0.02)
|
||||
events = (await client.get(f"/api/v1/runs/{spawned['run_id']}/events")).json()
|
||||
requested = next(e for e in events if e["type"] == "llm.requested")
|
||||
prompt = requested["payload"]["request"]["messages"][-1]["content"]
|
||||
assert "【世界认知(世谱命中)】" in prompt
|
||||
assert "星穹图书馆只在午后开放" in prompt
|
||||
@@ -0,0 +1,58 @@
|
||||
import { PageContainer } from "@/components/layout/PageContainer";
|
||||
import { ErrorState } from "@/components/shared/StateViews";
|
||||
import { MemoryGovernance } from "@/components/memories/MemoryGovernance";
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import { describeError } from "@/lib/utils/error";
|
||||
import type { Character } from "@/types/api";
|
||||
import type { FactualRecordView, XirangMemory } from "@/lib/api/memories";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function MemoriesPage() {
|
||||
let error: string | null = null;
|
||||
let characterId: string | undefined;
|
||||
|
||||
try {
|
||||
const active = await serverApi
|
||||
.get<Character>("/characters/active")
|
||||
.catch(() => null);
|
||||
characterId = active?.id;
|
||||
} catch (e) {
|
||||
error = describeError(e);
|
||||
}
|
||||
|
||||
if (error || !characterId) {
|
||||
return (
|
||||
<PageContainer
|
||||
title="记忆"
|
||||
description="三级记忆治理:确认、正影与删除。"
|
||||
>
|
||||
<ErrorState
|
||||
message={error ?? "尚未激活人物——启镜后这里会生长出记忆。"}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const [memories, records] = await Promise.all([
|
||||
serverApi
|
||||
.get<XirangMemory[]>(`/characters/${characterId}/memories`)
|
||||
.catch(() => []),
|
||||
serverApi
|
||||
.get<FactualRecordView[]>(`/characters/${characterId}/factual-records`)
|
||||
.catch(() => []),
|
||||
]);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="记忆"
|
||||
description="候选箱审阅 · 重忆确认 · 正影与删除 · 实录溯源。删除权高于召回便利。"
|
||||
>
|
||||
<MemoryGovernance
|
||||
characterId={characterId}
|
||||
initialMemories={memories}
|
||||
initialRecords={records}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { LogoButton } from "@/components/easter-egg/LogoButton";
|
||||
import {
|
||||
AdminIcon,
|
||||
ArchiveIcon,
|
||||
MemoryIcon,
|
||||
ChatIcon,
|
||||
CharacterIcon,
|
||||
CosmosIcon,
|
||||
@@ -84,7 +85,10 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
},
|
||||
{
|
||||
title: "藏识",
|
||||
items: [{ href: "/archive", label: "归档", icon: <ArchiveIcon /> }],
|
||||
items: [
|
||||
{ href: "/memory", label: "记忆", icon: <MemoryIcon /> },
|
||||
{ href: "/archive", label: "归档", icon: <ArchiveIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "百工",
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 记忆治理:候选箱审阅、重忆确认、正影、删除、来源展开、实录查看。
|
||||
* 治理红线:删除权与来源可见性高于召回便利;墓碑与高敏不展示原文。
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import * as memoriesApi from "@/lib/api/memories";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
import type { FactualRecordView, XirangMemory } from "@/lib/api/memories";
|
||||
|
||||
interface MemoryGovernanceProps {
|
||||
characterId: string;
|
||||
initialMemories: XirangMemory[];
|
||||
initialRecords: FactualRecordView[];
|
||||
}
|
||||
|
||||
const TIER_LABEL: Record<string, string> = {
|
||||
trace: "微痕",
|
||||
explicit: "明忆",
|
||||
sensitive: "重忆",
|
||||
};
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
persona: "画像",
|
||||
episodic: "事件",
|
||||
instruction: "指令",
|
||||
relationship_view: "心痕",
|
||||
fact_candidate: "手记",
|
||||
};
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
candidate: "候选箱",
|
||||
pending: "待确认",
|
||||
active: "已生效",
|
||||
superseded: "已正影",
|
||||
tombstoned: "已删除",
|
||||
};
|
||||
|
||||
type Tab = "pending" | "candidate" | "active" | "archive" | "records";
|
||||
|
||||
export function MemoryGovernance({
|
||||
characterId,
|
||||
initialMemories,
|
||||
initialRecords,
|
||||
}: MemoryGovernanceProps) {
|
||||
const [tab, setTab] = useState<Tab>("pending");
|
||||
const [items, setItems] = useState(initialMemories);
|
||||
const [records] = useState(initialRecords);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [correctTarget, setCorrectTarget] = useState<XirangMemory | null>(null);
|
||||
const [correctValue, setCorrectValue] = useState("");
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setItems(await memoriesApi.listMemories(characterId));
|
||||
}, [characterId]);
|
||||
|
||||
const run = async (action: () => Promise<unknown>) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await action();
|
||||
await reload();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const byTab = (status: XirangMemory["status"]) =>
|
||||
items.filter((m) => m.status === status);
|
||||
const visible: XirangMemory[] =
|
||||
tab === "pending"
|
||||
? byTab("pending")
|
||||
: tab === "candidate"
|
||||
? byTab("candidate")
|
||||
: tab === "active"
|
||||
? byTab("active")
|
||||
: tab === "archive"
|
||||
? items.filter(
|
||||
(m) => m.status === "superseded" || m.status === "tombstoned",
|
||||
)
|
||||
: [];
|
||||
|
||||
const tabs: { key: Tab; label: string; count: number }[] = [
|
||||
{ key: "pending", label: "待确认", count: byTab("pending").length },
|
||||
{ key: "candidate", label: "候选箱", count: byTab("candidate").length },
|
||||
{ key: "active", label: "已生效", count: byTab("active").length },
|
||||
{
|
||||
key: "archive",
|
||||
label: "归档",
|
||||
count: items.filter(
|
||||
(m) => m.status === "superseded" || m.status === "tombstoned",
|
||||
).length,
|
||||
},
|
||||
{ key: "records", label: "实录", count: records.length },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="记忆分区"
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={cn(
|
||||
"rounded-full border px-3 py-1 text-sm transition-colors",
|
||||
tab === t.key
|
||||
? "border-patina bg-patina/15 text-patina"
|
||||
: "border-rule text-muted hover:bg-overlay-hover",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
<span className="ml-1.5 text-xs opacity-70">{t.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tab === "records" ? (
|
||||
<ul className="space-y-2">
|
||||
{records.length === 0 && (
|
||||
<li className="rounded-xl border border-dashed border-rule p-5 text-center text-sm text-muted">
|
||||
尚无实录——镜树归真后自动生成。
|
||||
</li>
|
||||
)}
|
||||
{records.map((r) => (
|
||||
<li
|
||||
key={r.id}
|
||||
className="rounded-xl border border-rule bg-surface p-4"
|
||||
>
|
||||
<p className="text-sm">{r.summary}</p>
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
{new Date(r.occurred_at).toLocaleString("zh-CN")} · 证据{" "}
|
||||
{r.evidence.path_nodes?.length ?? 0} 节点
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{visible.length === 0 && (
|
||||
<li className="rounded-xl border border-dashed border-rule p-5 text-center text-sm text-muted">
|
||||
此分区暂无记忆。
|
||||
</li>
|
||||
)}
|
||||
{visible.map((m) => (
|
||||
<li
|
||||
key={m.id}
|
||||
className="rounded-xl border border-rule bg-surface p-4"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded px-1.5 py-0.5",
|
||||
m.tier === "sensitive" &&
|
||||
"bg-amber-500/15 text-amber-600 dark:text-amber-400",
|
||||
m.tier !== "sensitive" && "bg-muted text-muted",
|
||||
)}
|
||||
>
|
||||
{TIER_LABEL[m.tier] ?? m.tier}
|
||||
</span>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-muted">
|
||||
{TYPE_LABEL[m.type] ?? m.type}
|
||||
</span>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-muted">
|
||||
{STATUS_LABEL[m.status] ?? m.status}
|
||||
</span>
|
||||
{m.recall_count > 0 && (
|
||||
<span className="text-muted">召回 {m.recall_count} 次</span>
|
||||
)}
|
||||
<span className="ml-auto text-muted">
|
||||
{new Date(m.created_at).toLocaleString("zh-CN")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-relaxed">
|
||||
{m.content || "(原文已随删除移除)"}
|
||||
</p>
|
||||
{m.source_refs.length > 0 && (
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
来源:{m.source_refs.join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
{(m.status === "pending" ||
|
||||
m.status === "candidate" ||
|
||||
m.status === "active") && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{(m.status === "pending" || m.status === "candidate") && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void run(() => memoriesApi.confirmMemory(m.id))
|
||||
}
|
||||
>
|
||||
确认生效
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
setCorrectTarget(m);
|
||||
setCorrectValue(m.content);
|
||||
}}
|
||||
>
|
||||
正影
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => void run(() => memoriesApi.denyMemory(m.id))}
|
||||
>
|
||||
拒绝
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm("删除后全链路停用且不可恢复原文,确认?")
|
||||
) {
|
||||
void run(() => memoriesApi.deleteMemory(m.id));
|
||||
}
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={Boolean(correctTarget)}
|
||||
onClose={() => setCorrectTarget(null)}
|
||||
title="正影(形成新版本)"
|
||||
size="md"
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p className="text-muted">
|
||||
正影不改写历史:新版本成为召回当前版,旧版保留来源与纠错轨迹、停止普通召回。
|
||||
</p>
|
||||
<textarea
|
||||
value={correctValue}
|
||||
onChange={(e) => setCorrectValue(e.target.value)}
|
||||
rows={5}
|
||||
className="w-full resize-none rounded-lg border border-rule bg-transparent p-2 text-sm outline-none focus:border-patina"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCorrectTarget(null)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={busy || !correctValue.trim() || !correctTarget}
|
||||
onClick={() => {
|
||||
if (correctTarget) {
|
||||
void run(async () => {
|
||||
await memoriesApi.correctMemory(
|
||||
correctTarget.id,
|
||||
correctValue.trim(),
|
||||
);
|
||||
setCorrectTarget(null);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
创建新版本
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export * as settingsApi from "./settings";
|
||||
export * as importsApi from "./imports";
|
||||
export * as extractionApi from "./extraction";
|
||||
export * as worldApi from "./world";
|
||||
export * as memoriesApi from "./memories";
|
||||
export * as lorebooksApi from "./lorebooks";
|
||||
export * as skillsApi from "./skills";
|
||||
export * as mcpApi from "./mcp";
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
apiDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
import { confirmMemory, correctMemory, listMemories } from "./memories";
|
||||
|
||||
describe("memories api", () => {
|
||||
it("listMemories GET /characters/:id/memories 带筛选", async () => {
|
||||
vi.mocked(apiGet).mockResolvedValue([]);
|
||||
await listMemories("character_1", { status: "pending" });
|
||||
expect(apiGet).toHaveBeenCalledWith("/characters/character_1/memories", {
|
||||
status: "pending",
|
||||
});
|
||||
});
|
||||
|
||||
it("confirmMemory POST /memories/:id/confirm", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "memory_1", status: "active" });
|
||||
await confirmMemory("memory_1");
|
||||
expect(apiPost).toHaveBeenCalledWith("/memories/memory_1/confirm");
|
||||
});
|
||||
|
||||
it("correctMemory POST /memories/:id/correct", async () => {
|
||||
vi.mocked(apiPost).mockResolvedValue({ id: "memory_2" });
|
||||
await correctMemory("memory_1", "新版本");
|
||||
expect(apiPost).toHaveBeenCalledWith("/memories/memory_1/correct", {
|
||||
new_content: "新版本",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/** 息壤记忆 API Client(api-contracts §3.5)。 */
|
||||
|
||||
import { apiDelete, apiGet, apiPost } from "./client";
|
||||
|
||||
export interface XirangMemory {
|
||||
id: string;
|
||||
character_id: string;
|
||||
tier: "trace" | "explicit" | "sensitive";
|
||||
type: string;
|
||||
status: "candidate" | "pending" | "active" | "superseded" | "tombstoned";
|
||||
source: string;
|
||||
content: string;
|
||||
source_refs: string[];
|
||||
sensitivity: string;
|
||||
priority: number;
|
||||
confidence: number;
|
||||
vitality: number;
|
||||
realm: string;
|
||||
prev_version_id: string | null;
|
||||
recall_count: number;
|
||||
last_recalled_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface FactualRecordView {
|
||||
id: string;
|
||||
canonization_id: string;
|
||||
summary: string;
|
||||
evidence: { path_nodes?: string[] };
|
||||
occurred_at: string;
|
||||
}
|
||||
|
||||
export function listMemories(
|
||||
characterId: string,
|
||||
filters?: { tier?: string; status?: string; type?: string },
|
||||
) {
|
||||
return apiGet<XirangMemory[]>(`/characters/${characterId}/memories`, filters);
|
||||
}
|
||||
|
||||
export function confirmMemory(memoryId: string) {
|
||||
return apiPost<XirangMemory>(`/memories/${memoryId}/confirm`);
|
||||
}
|
||||
|
||||
export function denyMemory(memoryId: string) {
|
||||
return apiPost<{ memory_id: string; exited_retrieval: boolean }>(
|
||||
`/memories/${memoryId}/deny`,
|
||||
);
|
||||
}
|
||||
|
||||
export function correctMemory(memoryId: string, newContent: string) {
|
||||
return apiPost<XirangMemory>(`/memories/${memoryId}/correct`, {
|
||||
new_content: newContent,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteMemory(memoryId: string) {
|
||||
return apiDelete<void>(`/memories/${memoryId}`);
|
||||
}
|
||||
|
||||
export function listFactualRecords(characterId: string) {
|
||||
return apiGet<FactualRecordView[]>(
|
||||
`/characters/${characterId}/factual-records`,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user